diff --git a/examples-testing/examples/css2d_label.ts b/examples-testing/examples/css2d_label.ts new file mode 100644 index 000000000..48a2d1f05 --- /dev/null +++ b/examples-testing/examples/css2d_label.ts @@ -0,0 +1,186 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let gui; + +let camera, scene, renderer, labelRenderer; + +const layers = { + 'Toggle Name': function () { + camera.layers.toggle(0); + }, + 'Toggle Mass': function () { + camera.layers.toggle(1); + }, + 'Enable All': function () { + camera.layers.enableAll(); + }, + + 'Disable All': function () { + camera.layers.disableAll(); + }, +}; + +const clock = new THREE.Clock(); +const textureLoader = new THREE.TextureLoader(); + +let moon; + +init(); +animate(); + +function init() { + const EARTH_RADIUS = 1; + const MOON_RADIUS = 0.27; + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200); + camera.position.set(10, 5, 20); + camera.layers.enableAll(); + + scene = new THREE.Scene(); + + const dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.position.set(0, 0, 1); + dirLight.layers.enableAll(); + scene.add(dirLight); + + const axesHelper = new THREE.AxesHelper(5); + axesHelper.layers.enableAll(); + scene.add(axesHelper); + + // + + const earthGeometry = new THREE.SphereGeometry(EARTH_RADIUS, 16, 16); + const earthMaterial = new THREE.MeshPhongMaterial({ + specular: 0x333333, + shininess: 5, + map: textureLoader.load('textures/planets/earth_atmos_2048.jpg'), + specularMap: textureLoader.load('textures/planets/earth_specular_2048.jpg'), + normalMap: textureLoader.load('textures/planets/earth_normal_2048.jpg'), + normalScale: new THREE.Vector2(0.85, 0.85), + }); + earthMaterial.map.colorSpace = THREE.SRGBColorSpace; + const earth = new THREE.Mesh(earthGeometry, earthMaterial); + scene.add(earth); + + const moonGeometry = new THREE.SphereGeometry(MOON_RADIUS, 16, 16); + const moonMaterial = new THREE.MeshPhongMaterial({ + shininess: 5, + map: textureLoader.load('textures/planets/moon_1024.jpg'), + }); + moonMaterial.map.colorSpace = THREE.SRGBColorSpace; + moon = new THREE.Mesh(moonGeometry, moonMaterial); + scene.add(moon); + + // + + earth.layers.enableAll(); + moon.layers.enableAll(); + + const earthDiv = document.createElement('div'); + earthDiv.className = 'label'; + earthDiv.textContent = 'Earth'; + earthDiv.style.backgroundColor = 'transparent'; + + const earthLabel = new CSS2DObject(earthDiv); + earthLabel.position.set(1.5 * EARTH_RADIUS, 0, 0); + earthLabel.center.set(0, 1); + earth.add(earthLabel); + earthLabel.layers.set(0); + + const earthMassDiv = document.createElement('div'); + earthMassDiv.className = 'label'; + earthMassDiv.textContent = '5.97237e24 kg'; + earthMassDiv.style.backgroundColor = 'transparent'; + + const earthMassLabel = new CSS2DObject(earthMassDiv); + earthMassLabel.position.set(1.5 * EARTH_RADIUS, 0, 0); + earthMassLabel.center.set(0, 0); + earth.add(earthMassLabel); + earthMassLabel.layers.set(1); + + const moonDiv = document.createElement('div'); + moonDiv.className = 'label'; + moonDiv.textContent = 'Moon'; + moonDiv.style.backgroundColor = 'transparent'; + + const moonLabel = new CSS2DObject(moonDiv); + moonLabel.position.set(1.5 * MOON_RADIUS, 0, 0); + moonLabel.center.set(0, 1); + moon.add(moonLabel); + moonLabel.layers.set(0); + + const moonMassDiv = document.createElement('div'); + moonMassDiv.className = 'label'; + moonMassDiv.textContent = '7.342e22 kg'; + moonMassDiv.style.backgroundColor = 'transparent'; + + const moonMassLabel = new CSS2DObject(moonMassDiv); + moonMassLabel.position.set(1.5 * MOON_RADIUS, 0, 0); + moonMassLabel.center.set(0, 0); + moon.add(moonMassLabel); + moonMassLabel.layers.set(1); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + labelRenderer = new CSS2DRenderer(); + labelRenderer.setSize(window.innerWidth, window.innerHeight); + labelRenderer.domElement.style.position = 'absolute'; + labelRenderer.domElement.style.top = '0px'; + document.body.appendChild(labelRenderer.domElement); + + const controls = new OrbitControls(camera, labelRenderer.domElement); + controls.minDistance = 5; + controls.maxDistance = 100; + + // + + window.addEventListener('resize', onWindowResize); + + initGui(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + labelRenderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + requestAnimationFrame(animate); + + const elapsed = clock.getElapsedTime(); + + moon.position.set(Math.sin(elapsed) * 5, 0, Math.cos(elapsed) * 5); + + renderer.render(scene, camera); + labelRenderer.render(scene, camera); +} + +// + +function initGui() { + gui = new GUI(); + + gui.title('Camera Layers'); + + gui.add(layers, 'Toggle Name'); + gui.add(layers, 'Toggle Mass'); + gui.add(layers, 'Enable All'); + gui.add(layers, 'Disable All'); + + gui.open(); +} diff --git a/examples-testing/examples/css3d_molecules.ts b/examples-testing/examples/css3d_molecules.ts new file mode 100644 index 000000000..538472607 --- /dev/null +++ b/examples-testing/examples/css3d_molecules.ts @@ -0,0 +1,353 @@ +import * as THREE from 'three'; + +import { TrackballControls } from 'three/addons/controls/TrackballControls.js'; +import { PDBLoader } from 'three/addons/loaders/PDBLoader.js'; +import { CSS3DRenderer, CSS3DObject, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer; +let controls; +let root; + +const objects = []; +const tmpVec1 = new THREE.Vector3(); +const tmpVec2 = new THREE.Vector3(); +const tmpVec3 = new THREE.Vector3(); +const tmpVec4 = new THREE.Vector3(); +const offset = new THREE.Vector3(); + +const VIZ_TYPE = { + Atoms: 0, + Bonds: 1, + 'Atoms + Bonds': 2, +}; + +const MOLECULES = { + Ethanol: 'ethanol.pdb', + Aspirin: 'aspirin.pdb', + Caffeine: 'caffeine.pdb', + Nicotine: 'nicotine.pdb', + LSD: 'lsd.pdb', + Cocaine: 'cocaine.pdb', + Cholesterol: 'cholesterol.pdb', + Lycopene: 'lycopene.pdb', + Glucose: 'glucose.pdb', + 'Aluminium oxide': 'Al2O3.pdb', + Cubane: 'cubane.pdb', + Copper: 'cu.pdb', + Fluorite: 'caf2.pdb', + Salt: 'nacl.pdb', + 'YBCO superconductor': 'ybco.pdb', + Buckyball: 'buckyball.pdb', + // 'Diamond': 'diamond.pdb', + Graphite: 'graphite.pdb', +}; + +const params = { + vizType: 2, + molecule: 'caffeine.pdb', +}; + +const loader = new PDBLoader(); +const colorSpriteMap = {}; +const baseSprite = document.createElement('img'); + +init(); +animate(); + +function init() { + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 5000); + camera.position.z = 1000; + + scene = new THREE.Scene(); + + root = new THREE.Object3D(); + scene.add(root); + + // + + renderer = new CSS3DRenderer(); + renderer.setSize(window.innerWidth, window.innerHeight); + document.getElementById('container').appendChild(renderer.domElement); + + // + + controls = new TrackballControls(camera, renderer.domElement); + controls.rotateSpeed = 0.5; + + // + + baseSprite.onload = function () { + loadMolecule(params.molecule); + }; + + baseSprite.src = 'textures/sprites/ball.png'; + + // + + window.addEventListener('resize', onWindowResize); + + // + + const gui = new GUI(); + + gui.add(params, 'vizType', VIZ_TYPE).onChange(changeVizType); + gui.add(params, 'molecule', MOLECULES).onChange(loadMolecule); + gui.open(); +} + +function changeVizType(value) { + if (value === 0) showAtoms(); + else if (value === 1) showBonds(); + else showAtomsBonds(); +} + +// + +function showAtoms() { + for (let i = 0; i < objects.length; i++) { + const object = objects[i]; + + if (object instanceof CSS3DSprite) { + object.element.style.display = ''; + object.visible = true; + } else { + object.element.style.display = 'none'; + object.visible = false; + } + } +} + +function showBonds() { + for (let i = 0; i < objects.length; i++) { + const object = objects[i]; + + if (object instanceof CSS3DSprite) { + object.element.style.display = 'none'; + object.visible = false; + } else { + object.element.style.display = ''; + object.element.style.height = object.userData.bondLengthFull; + object.visible = true; + } + } +} + +function showAtomsBonds() { + for (let i = 0; i < objects.length; i++) { + const object = objects[i]; + + object.element.style.display = ''; + object.visible = true; + + if (!(object instanceof CSS3DSprite)) { + object.element.style.height = object.userData.bondLengthShort; + } + } +} + +// + +function colorify(ctx, width, height, color) { + const r = color.r, + g = color.g, + b = color.b; + + const imageData = ctx.getImageData(0, 0, width, height); + const data = imageData.data; + + for (let i = 0, l = data.length; i < l; i += 4) { + data[i + 0] *= r; + data[i + 1] *= g; + data[i + 2] *= b; + } + + ctx.putImageData(imageData, 0, 0); +} + +function imageToCanvas(image) { + const width = image.width; + const height = image.height; + + const canvas = document.createElement('canvas'); + + canvas.width = width; + canvas.height = height; + + const context = canvas.getContext('2d'); + context.drawImage(image, 0, 0, width, height); + + return canvas; +} + +// + +function loadMolecule(model) { + const url = 'models/pdb/' + model; + + for (let i = 0; i < objects.length; i++) { + const object = objects[i]; + object.parent.remove(object); + } + + objects.length = 0; + + loader.load(url, function (pdb) { + const geometryAtoms = pdb.geometryAtoms; + const geometryBonds = pdb.geometryBonds; + const json = pdb.json; + + geometryAtoms.computeBoundingBox(); + geometryAtoms.boundingBox.getCenter(offset).negate(); + + geometryAtoms.translate(offset.x, offset.y, offset.z); + geometryBonds.translate(offset.x, offset.y, offset.z); + + const positionAtoms = geometryAtoms.getAttribute('position'); + const colorAtoms = geometryAtoms.getAttribute('color'); + + const position = new THREE.Vector3(); + const color = new THREE.Color(); + + for (let i = 0; i < positionAtoms.count; i++) { + position.fromBufferAttribute(positionAtoms, i); + color.fromBufferAttribute(colorAtoms, i); + + const atomJSON = json.atoms[i]; + const element = atomJSON[4]; + + if (!colorSpriteMap[element]) { + const canvas = imageToCanvas(baseSprite); + const context = canvas.getContext('2d'); + + colorify(context, canvas.width, canvas.height, color); + + const dataUrl = canvas.toDataURL(); + + colorSpriteMap[element] = dataUrl; + } + + const colorSprite = colorSpriteMap[element]; + + const atom = document.createElement('img'); + atom.src = colorSprite; + + const object = new CSS3DSprite(atom); + object.position.copy(position); + object.position.multiplyScalar(75); + + object.matrixAutoUpdate = false; + object.updateMatrix(); + + root.add(object); + + objects.push(object); + } + + const positionBonds = geometryBonds.getAttribute('position'); + + const start = new THREE.Vector3(); + const end = new THREE.Vector3(); + + for (let i = 0; i < positionBonds.count; i += 2) { + start.fromBufferAttribute(positionBonds, i); + end.fromBufferAttribute(positionBonds, i + 1); + + start.multiplyScalar(75); + end.multiplyScalar(75); + + tmpVec1.subVectors(end, start); + const bondLength = tmpVec1.length() - 50; + + // + + let bond = document.createElement('div'); + bond.className = 'bond'; + bond.style.height = bondLength + 'px'; + + let object = new CSS3DObject(bond); + object.position.copy(start); + object.position.lerp(end, 0.5); + + object.userData.bondLengthShort = bondLength + 'px'; + object.userData.bondLengthFull = bondLength + 55 + 'px'; + + // + + const axis = tmpVec2.set(0, 1, 0).cross(tmpVec1); + const radians = Math.acos(tmpVec3.set(0, 1, 0).dot(tmpVec4.copy(tmpVec1).normalize())); + + const objMatrix = new THREE.Matrix4().makeRotationAxis(axis.normalize(), radians); + object.matrix.copy(objMatrix); + object.quaternion.setFromRotationMatrix(object.matrix); + + object.matrixAutoUpdate = false; + object.updateMatrix(); + + root.add(object); + + objects.push(object); + + // + + const joint = new THREE.Object3D(); + joint.position.copy(start); + joint.position.lerp(end, 0.5); + + joint.matrix.copy(objMatrix); + joint.quaternion.setFromRotationMatrix(joint.matrix); + + joint.matrixAutoUpdate = false; + joint.updateMatrix(); + + bond = document.createElement('div'); + bond.className = 'bond'; + bond.style.height = bondLength + 'px'; + + object = new CSS3DObject(bond); + object.rotation.y = Math.PI / 2; + + object.matrixAutoUpdate = false; + object.updateMatrix(); + + object.userData.bondLengthShort = bondLength + 'px'; + object.userData.bondLengthFull = bondLength + 55 + 'px'; + + object.userData.joint = joint; + + joint.add(object); + root.add(joint); + + objects.push(object); + } + + //console.log( "CSS3DObjects:", objects.length ); + + changeVizType(params.vizType); + }); +} + +// + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + requestAnimationFrame(animate); + controls.update(); + + const time = Date.now() * 0.0004; + + root.rotation.x = time; + root.rotation.y = time * 0.7; + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/css3d_orthographic.ts b/examples-testing/examples/css3d_orthographic.ts new file mode 100644 index 000000000..4aabbed08 --- /dev/null +++ b/examples-testing/examples/css3d_orthographic.ts @@ -0,0 +1,208 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer; + +let scene2, renderer2; + +const frustumSize = 500; + +init(); +animate(); + +function init() { + const aspect = window.innerWidth / window.innerHeight; + camera = new THREE.OrthographicCamera( + (frustumSize * aspect) / -2, + (frustumSize * aspect) / 2, + frustumSize / 2, + frustumSize / -2, + 1, + 1000, + ); + + camera.position.set(-200, 200, 200); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + scene2 = new THREE.Scene(); + + const material = new THREE.MeshBasicMaterial({ + color: 0x000000, + wireframe: true, + wireframeLinewidth: 1, + side: THREE.DoubleSide, + }); + + // left + createPlane( + 100, + 100, + 'chocolate', + new THREE.Vector3(-50, 0, 0), + new THREE.Euler(0, -90 * THREE.MathUtils.DEG2RAD, 0), + ); + // right + createPlane(100, 100, 'saddlebrown', new THREE.Vector3(0, 0, 50), new THREE.Euler(0, 0, 0)); + // top + createPlane( + 100, + 100, + 'yellowgreen', + new THREE.Vector3(0, 50, 0), + new THREE.Euler(-90 * THREE.MathUtils.DEG2RAD, 0, 0), + ); + // bottom + createPlane( + 300, + 300, + 'seagreen', + new THREE.Vector3(0, -50, 0), + new THREE.Euler(-90 * THREE.MathUtils.DEG2RAD, 0, 0), + ); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + renderer2 = new CSS3DRenderer(); + renderer2.setSize(window.innerWidth, window.innerHeight); + renderer2.domElement.style.position = 'absolute'; + renderer2.domElement.style.top = 0; + document.body.appendChild(renderer2.domElement); + + const controls = new OrbitControls(camera, renderer2.domElement); + controls.minZoom = 0.5; + controls.maxZoom = 2; + + function createPlane(width, height, cssColor, pos, rot) { + const element = document.createElement('div'); + element.style.width = width + 'px'; + element.style.height = height + 'px'; + element.style.opacity = 0.75; + element.style.background = cssColor; + + const object = new CSS3DObject(element); + object.position.copy(pos); + object.rotation.copy(rot); + scene2.add(object); + + const geometry = new THREE.PlaneGeometry(width, height); + const mesh = new THREE.Mesh(geometry, material); + mesh.position.copy(object.position); + mesh.rotation.copy(object.rotation); + scene.add(mesh); + } + + window.addEventListener('resize', onWindowResize); + createPanel(); +} + +function onWindowResize() { + const aspect = window.innerWidth / window.innerHeight; + + camera.left = (-frustumSize * aspect) / 2; + camera.right = (frustumSize * aspect) / 2; + camera.top = frustumSize / 2; + camera.bottom = -frustumSize / 2; + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + renderer2.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + requestAnimationFrame(animate); + + renderer.render(scene, camera); + renderer2.render(scene2, camera); +} + +function createPanel() { + const panel = new GUI(); + const folder1 = panel.addFolder('camera setViewOffset').close(); + + const settings = { + setViewOffset() { + folder1.children[1].enable().setValue(window.innerWidth); + folder1.children[2].enable().setValue(window.innerHeight); + folder1.children[3].enable().setValue(0); + folder1.children[4].enable().setValue(0); + folder1.children[5].enable().setValue(window.innerWidth); + folder1.children[6].enable().setValue(window.innerHeight); + }, + fullWidth: 0, + fullHeight: 0, + offsetX: 0, + offsetY: 0, + width: 0, + height: 0, + clearViewOffset() { + folder1.children[1].setValue(0).disable(); + folder1.children[2].setValue(0).disable(); + folder1.children[3].setValue(0).disable(); + folder1.children[4].setValue(0).disable(); + folder1.children[5].setValue(0).disable(); + folder1.children[6].setValue(0).disable(); + camera.clearViewOffset(); + }, + }; + + folder1.add(settings, 'setViewOffset'); + folder1 + .add(settings, 'fullWidth', window.screen.width / 4, window.screen.width * 2, 1) + .onChange(val => updateCameraViewOffset({ fullWidth: val })) + .disable(); + folder1 + .add(settings, 'fullHeight', window.screen.height / 4, window.screen.height * 2, 1) + .onChange(val => updateCameraViewOffset({ fullHeight: val })) + .disable(); + folder1 + .add(settings, 'offsetX', 0, 256, 1) + .onChange(val => updateCameraViewOffset({ x: val })) + .disable(); + folder1 + .add(settings, 'offsetY', 0, 256, 1) + .onChange(val => updateCameraViewOffset({ y: val })) + .disable(); + folder1 + .add(settings, 'width', window.screen.width / 4, window.screen.width * 2, 1) + .onChange(val => updateCameraViewOffset({ width: val })) + .disable(); + folder1 + .add(settings, 'height', window.screen.height / 4, window.screen.height * 2, 1) + .onChange(val => updateCameraViewOffset({ height: val })) + .disable(); + folder1.add(settings, 'clearViewOffset'); +} + +function updateCameraViewOffset({ fullWidth, fullHeight, x, y, width, height }) { + if (!camera.view) { + camera.setViewOffset( + fullWidth || window.innerWidth, + fullHeight || window.innerHeight, + x || 0, + y || 0, + width || window.innerWidth, + height || window.innerHeight, + ); + } else { + camera.setViewOffset( + fullWidth || camera.view.fullWidth, + fullHeight || camera.view.fullHeight, + x || camera.view.offsetX, + y || camera.view.offsetY, + width || camera.view.width, + height || camera.view.height, + ); + } +} diff --git a/examples-testing/examples/css3d_periodictable.ts b/examples-testing/examples/css3d_periodictable.ts new file mode 100644 index 000000000..e3a33f796 --- /dev/null +++ b/examples-testing/examples/css3d_periodictable.ts @@ -0,0 +1,793 @@ +import * as THREE from 'three'; + +import TWEEN from 'three/addons/libs/tween.module.js'; +import { TrackballControls } from 'three/addons/controls/TrackballControls.js'; +import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js'; + +const table = [ + 'H', + 'Hydrogen', + '1.00794', + 1, + 1, + 'He', + 'Helium', + '4.002602', + 18, + 1, + 'Li', + 'Lithium', + '6.941', + 1, + 2, + 'Be', + 'Beryllium', + '9.012182', + 2, + 2, + 'B', + 'Boron', + '10.811', + 13, + 2, + 'C', + 'Carbon', + '12.0107', + 14, + 2, + 'N', + 'Nitrogen', + '14.0067', + 15, + 2, + 'O', + 'Oxygen', + '15.9994', + 16, + 2, + 'F', + 'Fluorine', + '18.9984032', + 17, + 2, + 'Ne', + 'Neon', + '20.1797', + 18, + 2, + 'Na', + 'Sodium', + '22.98976...', + 1, + 3, + 'Mg', + 'Magnesium', + '24.305', + 2, + 3, + 'Al', + 'Aluminium', + '26.9815386', + 13, + 3, + 'Si', + 'Silicon', + '28.0855', + 14, + 3, + 'P', + 'Phosphorus', + '30.973762', + 15, + 3, + 'S', + 'Sulfur', + '32.065', + 16, + 3, + 'Cl', + 'Chlorine', + '35.453', + 17, + 3, + 'Ar', + 'Argon', + '39.948', + 18, + 3, + 'K', + 'Potassium', + '39.948', + 1, + 4, + 'Ca', + 'Calcium', + '40.078', + 2, + 4, + 'Sc', + 'Scandium', + '44.955912', + 3, + 4, + 'Ti', + 'Titanium', + '47.867', + 4, + 4, + 'V', + 'Vanadium', + '50.9415', + 5, + 4, + 'Cr', + 'Chromium', + '51.9961', + 6, + 4, + 'Mn', + 'Manganese', + '54.938045', + 7, + 4, + 'Fe', + 'Iron', + '55.845', + 8, + 4, + 'Co', + 'Cobalt', + '58.933195', + 9, + 4, + 'Ni', + 'Nickel', + '58.6934', + 10, + 4, + 'Cu', + 'Copper', + '63.546', + 11, + 4, + 'Zn', + 'Zinc', + '65.38', + 12, + 4, + 'Ga', + 'Gallium', + '69.723', + 13, + 4, + 'Ge', + 'Germanium', + '72.63', + 14, + 4, + 'As', + 'Arsenic', + '74.9216', + 15, + 4, + 'Se', + 'Selenium', + '78.96', + 16, + 4, + 'Br', + 'Bromine', + '79.904', + 17, + 4, + 'Kr', + 'Krypton', + '83.798', + 18, + 4, + 'Rb', + 'Rubidium', + '85.4678', + 1, + 5, + 'Sr', + 'Strontium', + '87.62', + 2, + 5, + 'Y', + 'Yttrium', + '88.90585', + 3, + 5, + 'Zr', + 'Zirconium', + '91.224', + 4, + 5, + 'Nb', + 'Niobium', + '92.90628', + 5, + 5, + 'Mo', + 'Molybdenum', + '95.96', + 6, + 5, + 'Tc', + 'Technetium', + '(98)', + 7, + 5, + 'Ru', + 'Ruthenium', + '101.07', + 8, + 5, + 'Rh', + 'Rhodium', + '102.9055', + 9, + 5, + 'Pd', + 'Palladium', + '106.42', + 10, + 5, + 'Ag', + 'Silver', + '107.8682', + 11, + 5, + 'Cd', + 'Cadmium', + '112.411', + 12, + 5, + 'In', + 'Indium', + '114.818', + 13, + 5, + 'Sn', + 'Tin', + '118.71', + 14, + 5, + 'Sb', + 'Antimony', + '121.76', + 15, + 5, + 'Te', + 'Tellurium', + '127.6', + 16, + 5, + 'I', + 'Iodine', + '126.90447', + 17, + 5, + 'Xe', + 'Xenon', + '131.293', + 18, + 5, + 'Cs', + 'Caesium', + '132.9054', + 1, + 6, + 'Ba', + 'Barium', + '132.9054', + 2, + 6, + 'La', + 'Lanthanum', + '138.90547', + 4, + 9, + 'Ce', + 'Cerium', + '140.116', + 5, + 9, + 'Pr', + 'Praseodymium', + '140.90765', + 6, + 9, + 'Nd', + 'Neodymium', + '144.242', + 7, + 9, + 'Pm', + 'Promethium', + '(145)', + 8, + 9, + 'Sm', + 'Samarium', + '150.36', + 9, + 9, + 'Eu', + 'Europium', + '151.964', + 10, + 9, + 'Gd', + 'Gadolinium', + '157.25', + 11, + 9, + 'Tb', + 'Terbium', + '158.92535', + 12, + 9, + 'Dy', + 'Dysprosium', + '162.5', + 13, + 9, + 'Ho', + 'Holmium', + '164.93032', + 14, + 9, + 'Er', + 'Erbium', + '167.259', + 15, + 9, + 'Tm', + 'Thulium', + '168.93421', + 16, + 9, + 'Yb', + 'Ytterbium', + '173.054', + 17, + 9, + 'Lu', + 'Lutetium', + '174.9668', + 18, + 9, + 'Hf', + 'Hafnium', + '178.49', + 4, + 6, + 'Ta', + 'Tantalum', + '180.94788', + 5, + 6, + 'W', + 'Tungsten', + '183.84', + 6, + 6, + 'Re', + 'Rhenium', + '186.207', + 7, + 6, + 'Os', + 'Osmium', + '190.23', + 8, + 6, + 'Ir', + 'Iridium', + '192.217', + 9, + 6, + 'Pt', + 'Platinum', + '195.084', + 10, + 6, + 'Au', + 'Gold', + '196.966569', + 11, + 6, + 'Hg', + 'Mercury', + '200.59', + 12, + 6, + 'Tl', + 'Thallium', + '204.3833', + 13, + 6, + 'Pb', + 'Lead', + '207.2', + 14, + 6, + 'Bi', + 'Bismuth', + '208.9804', + 15, + 6, + 'Po', + 'Polonium', + '(209)', + 16, + 6, + 'At', + 'Astatine', + '(210)', + 17, + 6, + 'Rn', + 'Radon', + '(222)', + 18, + 6, + 'Fr', + 'Francium', + '(223)', + 1, + 7, + 'Ra', + 'Radium', + '(226)', + 2, + 7, + 'Ac', + 'Actinium', + '(227)', + 4, + 10, + 'Th', + 'Thorium', + '232.03806', + 5, + 10, + 'Pa', + 'Protactinium', + '231.0588', + 6, + 10, + 'U', + 'Uranium', + '238.02891', + 7, + 10, + 'Np', + 'Neptunium', + '(237)', + 8, + 10, + 'Pu', + 'Plutonium', + '(244)', + 9, + 10, + 'Am', + 'Americium', + '(243)', + 10, + 10, + 'Cm', + 'Curium', + '(247)', + 11, + 10, + 'Bk', + 'Berkelium', + '(247)', + 12, + 10, + 'Cf', + 'Californium', + '(251)', + 13, + 10, + 'Es', + 'Einstenium', + '(252)', + 14, + 10, + 'Fm', + 'Fermium', + '(257)', + 15, + 10, + 'Md', + 'Mendelevium', + '(258)', + 16, + 10, + 'No', + 'Nobelium', + '(259)', + 17, + 10, + 'Lr', + 'Lawrencium', + '(262)', + 18, + 10, + 'Rf', + 'Rutherfordium', + '(267)', + 4, + 7, + 'Db', + 'Dubnium', + '(268)', + 5, + 7, + 'Sg', + 'Seaborgium', + '(271)', + 6, + 7, + 'Bh', + 'Bohrium', + '(272)', + 7, + 7, + 'Hs', + 'Hassium', + '(270)', + 8, + 7, + 'Mt', + 'Meitnerium', + '(276)', + 9, + 7, + 'Ds', + 'Darmstadium', + '(281)', + 10, + 7, + 'Rg', + 'Roentgenium', + '(280)', + 11, + 7, + 'Cn', + 'Copernicium', + '(285)', + 12, + 7, + 'Nh', + 'Nihonium', + '(286)', + 13, + 7, + 'Fl', + 'Flerovium', + '(289)', + 14, + 7, + 'Mc', + 'Moscovium', + '(290)', + 15, + 7, + 'Lv', + 'Livermorium', + '(293)', + 16, + 7, + 'Ts', + 'Tennessine', + '(294)', + 17, + 7, + 'Og', + 'Oganesson', + '(294)', + 18, + 7, +]; + +let camera, scene, renderer; +let controls; + +const objects = []; +const targets = { table: [], sphere: [], helix: [], grid: [] }; + +init(); +animate(); + +function init() { + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 3000; + + scene = new THREE.Scene(); + + // table + + for (let i = 0; i < table.length; i += 5) { + const element = document.createElement('div'); + element.className = 'element'; + element.style.backgroundColor = 'rgba(0,127,127,' + (Math.random() * 0.5 + 0.25) + ')'; + + const number = document.createElement('div'); + number.className = 'number'; + number.textContent = i / 5 + 1; + element.appendChild(number); + + const symbol = document.createElement('div'); + symbol.className = 'symbol'; + symbol.textContent = table[i]; + element.appendChild(symbol); + + const details = document.createElement('div'); + details.className = 'details'; + details.innerHTML = table[i + 1] + '
' + table[i + 2]; + element.appendChild(details); + + const objectCSS = new CSS3DObject(element); + objectCSS.position.x = Math.random() * 4000 - 2000; + objectCSS.position.y = Math.random() * 4000 - 2000; + objectCSS.position.z = Math.random() * 4000 - 2000; + scene.add(objectCSS); + + objects.push(objectCSS); + + // + + const object = new THREE.Object3D(); + object.position.x = table[i + 3] * 140 - 1330; + object.position.y = -(table[i + 4] * 180) + 990; + + targets.table.push(object); + } + + // sphere + + const vector = new THREE.Vector3(); + + for (let i = 0, l = objects.length; i < l; i++) { + const phi = Math.acos(-1 + (2 * i) / l); + const theta = Math.sqrt(l * Math.PI) * phi; + + const object = new THREE.Object3D(); + + object.position.setFromSphericalCoords(800, phi, theta); + + vector.copy(object.position).multiplyScalar(2); + + object.lookAt(vector); + + targets.sphere.push(object); + } + + // helix + + for (let i = 0, l = objects.length; i < l; i++) { + const theta = i * 0.175 + Math.PI; + const y = -(i * 8) + 450; + + const object = new THREE.Object3D(); + + object.position.setFromCylindricalCoords(900, theta, y); + + vector.x = object.position.x * 2; + vector.y = object.position.y; + vector.z = object.position.z * 2; + + object.lookAt(vector); + + targets.helix.push(object); + } + + // grid + + for (let i = 0; i < objects.length; i++) { + const object = new THREE.Object3D(); + + object.position.x = (i % 5) * 400 - 800; + object.position.y = -(Math.floor(i / 5) % 5) * 400 + 800; + object.position.z = Math.floor(i / 25) * 1000 - 2000; + + targets.grid.push(object); + } + + // + + renderer = new CSS3DRenderer(); + renderer.setSize(window.innerWidth, window.innerHeight); + document.getElementById('container').appendChild(renderer.domElement); + + // + + controls = new TrackballControls(camera, renderer.domElement); + controls.minDistance = 500; + controls.maxDistance = 6000; + controls.addEventListener('change', render); + + const buttonTable = document.getElementById('table'); + buttonTable.addEventListener('click', function () { + transform(targets.table, 2000); + }); + + const buttonSphere = document.getElementById('sphere'); + buttonSphere.addEventListener('click', function () { + transform(targets.sphere, 2000); + }); + + const buttonHelix = document.getElementById('helix'); + buttonHelix.addEventListener('click', function () { + transform(targets.helix, 2000); + }); + + const buttonGrid = document.getElementById('grid'); + buttonGrid.addEventListener('click', function () { + transform(targets.grid, 2000); + }); + + transform(targets.table, 2000); + + // + + window.addEventListener('resize', onWindowResize); +} + +function transform(targets, duration) { + TWEEN.removeAll(); + + for (let i = 0; i < objects.length; i++) { + const object = objects[i]; + const target = targets[i]; + + new TWEEN.Tween(object.position) + .to( + { x: target.position.x, y: target.position.y, z: target.position.z }, + Math.random() * duration + duration, + ) + .easing(TWEEN.Easing.Exponential.InOut) + .start(); + + new TWEEN.Tween(object.rotation) + .to( + { x: target.rotation.x, y: target.rotation.y, z: target.rotation.z }, + Math.random() * duration + duration, + ) + .easing(TWEEN.Easing.Exponential.InOut) + .start(); + } + + new TWEEN.Tween(this) + .to({}, duration * 2) + .onUpdate(render) + .start(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function animate() { + requestAnimationFrame(animate); + + TWEEN.update(); + + controls.update(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/css3d_sandbox.ts b/examples-testing/examples/css3d_sandbox.ts new file mode 100644 index 000000000..1088b84b1 --- /dev/null +++ b/examples-testing/examples/css3d_sandbox.ts @@ -0,0 +1,180 @@ +import * as THREE from 'three'; + +import { TrackballControls } from 'three/addons/controls/TrackballControls.js'; +import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer; + +let scene2, renderer2; + +let controls; + +init(); +animate(); + +function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(200, 200, 200); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + scene2 = new THREE.Scene(); + + const material = new THREE.MeshBasicMaterial({ + color: 0x000000, + wireframe: true, + wireframeLinewidth: 1, + side: THREE.DoubleSide, + }); + + // + + for (let i = 0; i < 10; i++) { + const element = document.createElement('div'); + element.style.width = '100px'; + element.style.height = '100px'; + element.style.opacity = i < 5 ? 0.5 : 1; + element.style.background = new THREE.Color(Math.random() * 0xffffff).getStyle(); + + const object = new CSS3DObject(element); + object.position.x = Math.random() * 200 - 100; + object.position.y = Math.random() * 200 - 100; + object.position.z = Math.random() * 200 - 100; + object.rotation.x = Math.random(); + object.rotation.y = Math.random(); + object.rotation.z = Math.random(); + object.scale.x = Math.random() + 0.5; + object.scale.y = Math.random() + 0.5; + scene2.add(object); + + const geometry = new THREE.PlaneGeometry(100, 100); + const mesh = new THREE.Mesh(geometry, material); + mesh.position.copy(object.position); + mesh.rotation.copy(object.rotation); + mesh.scale.copy(object.scale); + scene.add(mesh); + } + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + renderer2 = new CSS3DRenderer(); + renderer2.setSize(window.innerWidth, window.innerHeight); + renderer2.domElement.style.position = 'absolute'; + renderer2.domElement.style.top = 0; + document.body.appendChild(renderer2.domElement); + + controls = new TrackballControls(camera, renderer2.domElement); + + window.addEventListener('resize', onWindowResize); + createPanel(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + renderer2.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + requestAnimationFrame(animate); + + controls.update(); + + renderer.render(scene, camera); + renderer2.render(scene2, camera); +} + +function createPanel() { + const panel = new GUI(); + const folder1 = panel.addFolder('camera setViewOffset').close(); + + const settings = { + setViewOffset() { + folder1.children[1].enable().setValue(window.innerWidth); + folder1.children[2].enable().setValue(window.innerHeight); + folder1.children[3].enable().setValue(0); + folder1.children[4].enable().setValue(0); + folder1.children[5].enable().setValue(window.innerWidth); + folder1.children[6].enable().setValue(window.innerHeight); + }, + fullWidth: 0, + fullHeight: 0, + offsetX: 0, + offsetY: 0, + width: 0, + height: 0, + clearViewOffset() { + folder1.children[1].setValue(0).disable(); + folder1.children[2].setValue(0).disable(); + folder1.children[3].setValue(0).disable(); + folder1.children[4].setValue(0).disable(); + folder1.children[5].setValue(0).disable(); + folder1.children[6].setValue(0).disable(); + camera.clearViewOffset(); + }, + }; + + folder1.add(settings, 'setViewOffset'); + folder1 + .add(settings, 'fullWidth', window.screen.width / 4, window.screen.width * 2, 1) + .onChange(val => updateCameraViewOffset({ fullWidth: val })) + .disable(); + folder1 + .add(settings, 'fullHeight', window.screen.height / 4, window.screen.height * 2, 1) + .onChange(val => updateCameraViewOffset({ fullHeight: val })) + .disable(); + folder1 + .add(settings, 'offsetX', 0, 256, 1) + .onChange(val => updateCameraViewOffset({ x: val })) + .disable(); + folder1 + .add(settings, 'offsetY', 0, 256, 1) + .onChange(val => updateCameraViewOffset({ y: val })) + .disable(); + folder1 + .add(settings, 'width', window.screen.width / 4, window.screen.width * 2, 1) + .onChange(val => updateCameraViewOffset({ width: val })) + .disable(); + folder1 + .add(settings, 'height', window.screen.height / 4, window.screen.height * 2, 1) + .onChange(val => updateCameraViewOffset({ height: val })) + .disable(); + folder1.add(settings, 'clearViewOffset'); +} + +function updateCameraViewOffset({ fullWidth, fullHeight, x, y, width, height }) { + if (!camera.view) { + camera.setViewOffset( + fullWidth || window.innerWidth, + fullHeight || window.innerHeight, + x || 0, + y || 0, + width || window.innerWidth, + height || window.innerHeight, + ); + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + } else { + camera.setViewOffset( + fullWidth || camera.view.fullWidth, + fullHeight || camera.view.fullHeight, + x || camera.view.offsetX, + y || camera.view.offsetY, + width || camera.view.width, + height || camera.view.height, + ); + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + } +} diff --git a/examples-testing/examples/css3d_sprites.ts b/examples-testing/examples/css3d_sprites.ts new file mode 100644 index 000000000..dfe24e79d --- /dev/null +++ b/examples-testing/examples/css3d_sprites.ts @@ -0,0 +1,157 @@ +import * as THREE from 'three'; + +import TWEEN from 'three/addons/libs/tween.module.js'; +import { TrackballControls } from 'three/addons/controls/TrackballControls.js'; +import { CSS3DRenderer, CSS3DSprite } from 'three/addons/renderers/CSS3DRenderer.js'; + +let camera, scene, renderer; +let controls; + +const particlesTotal = 512; +const positions = []; +const objects = []; +let current = 0; + +init(); +animate(); + +function init() { + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 5000); + camera.position.set(600, 400, 1500); + camera.lookAt(0, 0, 0); + + scene = new THREE.Scene(); + + const image = document.createElement('img'); + image.addEventListener('load', function () { + for (let i = 0; i < particlesTotal; i++) { + const object = new CSS3DSprite(image.cloneNode()); + (object.position.x = Math.random() * 4000 - 2000), + (object.position.y = Math.random() * 4000 - 2000), + (object.position.z = Math.random() * 4000 - 2000); + scene.add(object); + + objects.push(object); + } + + transition(); + }); + image.src = 'textures/sprite.png'; + + // Plane + + const amountX = 16; + const amountZ = 32; + const separationPlane = 150; + const offsetX = ((amountX - 1) * separationPlane) / 2; + const offsetZ = ((amountZ - 1) * separationPlane) / 2; + + for (let i = 0; i < particlesTotal; i++) { + const x = (i % amountX) * separationPlane; + const z = Math.floor(i / amountX) * separationPlane; + const y = (Math.sin(x * 0.5) + Math.sin(z * 0.5)) * 200; + + positions.push(x - offsetX, y, z - offsetZ); + } + + // Cube + + const amount = 8; + const separationCube = 150; + const offset = ((amount - 1) * separationCube) / 2; + + for (let i = 0; i < particlesTotal; i++) { + const x = (i % amount) * separationCube; + const y = Math.floor((i / amount) % amount) * separationCube; + const z = Math.floor(i / (amount * amount)) * separationCube; + + positions.push(x - offset, y - offset, z - offset); + } + + // Random + + for (let i = 0; i < particlesTotal; i++) { + positions.push(Math.random() * 4000 - 2000, Math.random() * 4000 - 2000, Math.random() * 4000 - 2000); + } + + // Sphere + + const radius = 750; + + for (let i = 0; i < particlesTotal; i++) { + const phi = Math.acos(-1 + (2 * i) / particlesTotal); + const theta = Math.sqrt(particlesTotal * Math.PI) * phi; + + positions.push( + radius * Math.cos(theta) * Math.sin(phi), + radius * Math.sin(theta) * Math.sin(phi), + radius * Math.cos(phi), + ); + } + + // + + renderer = new CSS3DRenderer(); + renderer.setSize(window.innerWidth, window.innerHeight); + document.getElementById('container').appendChild(renderer.domElement); + + // + + controls = new TrackballControls(camera, renderer.domElement); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function transition() { + const offset = current * particlesTotal * 3; + const duration = 2000; + + for (let i = 0, j = offset; i < particlesTotal; i++, j += 3) { + const object = objects[i]; + + new TWEEN.Tween(object.position) + .to( + { + x: positions[j], + y: positions[j + 1], + z: positions[j + 2], + }, + Math.random() * duration + duration, + ) + .easing(TWEEN.Easing.Exponential.InOut) + .start(); + } + + new TWEEN.Tween(this) + .to({}, duration * 3) + .onComplete(transition) + .start(); + + current = (current + 1) % 4; +} + +function animate() { + requestAnimationFrame(animate); + + TWEEN.update(); + controls.update(); + + const time = performance.now(); + + for (let i = 0, l = objects.length; i < l; i++) { + const object = objects[i]; + const scale = Math.sin((Math.floor(object.position.x) + time) * 0.002) * 0.3 + 1; + object.scale.set(scale, scale, scale); + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/css3d_youtube.ts b/examples-testing/examples/css3d_youtube.ts new file mode 100644 index 000000000..62652f87f --- /dev/null +++ b/examples-testing/examples/css3d_youtube.ts @@ -0,0 +1,79 @@ +import * as THREE from 'three'; + +import { TrackballControls } from 'three/addons/controls/TrackballControls.js'; +import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js'; + +let camera, scene, renderer; +let controls; + +function Element(id, x, y, z, ry) { + const div = document.createElement('div'); + div.style.width = '480px'; + div.style.height = '360px'; + div.style.backgroundColor = '#000'; + + const iframe = document.createElement('iframe'); + iframe.style.width = '480px'; + iframe.style.height = '360px'; + iframe.style.border = '0px'; + iframe.src = ['https://www.youtube.com/embed/', id, '?rel=0'].join(''); + div.appendChild(iframe); + + const object = new CSS3DObject(div); + object.position.set(x, y, z); + object.rotation.y = ry; + + return object; +} + +init(); +animate(); + +function init() { + const container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 5000); + camera.position.set(500, 350, 750); + + scene = new THREE.Scene(); + + renderer = new CSS3DRenderer(); + renderer.setSize(window.innerWidth, window.innerHeight); + container.appendChild(renderer.domElement); + + const group = new THREE.Group(); + group.add(new Element('SJOz3qjfQXU', 0, 0, 240, 0)); + group.add(new Element('Y2-xZ-1HE-Q', 240, 0, 0, Math.PI / 2)); + group.add(new Element('IrydklNpcFI', 0, 0, -240, Math.PI)); + group.add(new Element('9ubytEsCaS0', -240, 0, 0, -Math.PI / 2)); + scene.add(group); + + controls = new TrackballControls(camera, renderer.domElement); + controls.rotateSpeed = 4; + + window.addEventListener('resize', onWindowResize); + + // Block iframe events when dragging camera + + const blocker = document.getElementById('blocker'); + blocker.style.display = 'none'; + + controls.addEventListener('start', function () { + blocker.style.display = ''; + }); + controls.addEventListener('end', function () { + blocker.style.display = 'none'; + }); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + requestAnimationFrame(animate); + controls.update(); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/games_fps.ts b/examples-testing/examples/games_fps.ts new file mode 100644 index 000000000..4c459f9bc --- /dev/null +++ b/examples-testing/examples/games_fps.ts @@ -0,0 +1,372 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +import { Octree } from 'three/addons/math/Octree.js'; +import { OctreeHelper } from 'three/addons/helpers/OctreeHelper.js'; + +import { Capsule } from 'three/addons/math/Capsule.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +const clock = new THREE.Clock(); + +const scene = new THREE.Scene(); +scene.background = new THREE.Color(0x88ccee); +scene.fog = new THREE.Fog(0x88ccee, 0, 50); + +const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000); +camera.rotation.order = 'YXZ'; + +const fillLight1 = new THREE.HemisphereLight(0x8dc1de, 0x00668d, 1.5); +fillLight1.position.set(2, 1, 1); +scene.add(fillLight1); + +const directionalLight = new THREE.DirectionalLight(0xffffff, 2.5); +directionalLight.position.set(-5, 25, -1); +directionalLight.castShadow = true; +directionalLight.shadow.camera.near = 0.01; +directionalLight.shadow.camera.far = 500; +directionalLight.shadow.camera.right = 30; +directionalLight.shadow.camera.left = -30; +directionalLight.shadow.camera.top = 30; +directionalLight.shadow.camera.bottom = -30; +directionalLight.shadow.mapSize.width = 1024; +directionalLight.shadow.mapSize.height = 1024; +directionalLight.shadow.radius = 4; +directionalLight.shadow.bias = -0.00006; +scene.add(directionalLight); + +const container = document.getElementById('container'); + +const renderer = new THREE.WebGLRenderer({ antialias: true }); +renderer.setPixelRatio(window.devicePixelRatio); +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.setAnimationLoop(animate); +renderer.shadowMap.enabled = true; +renderer.shadowMap.type = THREE.VSMShadowMap; +renderer.toneMapping = THREE.ACESFilmicToneMapping; +container.appendChild(renderer.domElement); + +const stats = new Stats(); +stats.domElement.style.position = 'absolute'; +stats.domElement.style.top = '0px'; +container.appendChild(stats.domElement); + +const GRAVITY = 30; + +const NUM_SPHERES = 100; +const SPHERE_RADIUS = 0.2; + +const STEPS_PER_FRAME = 5; + +const sphereGeometry = new THREE.IcosahedronGeometry(SPHERE_RADIUS, 5); +const sphereMaterial = new THREE.MeshLambertMaterial({ color: 0xdede8d }); + +const spheres = []; +let sphereIdx = 0; + +for (let i = 0; i < NUM_SPHERES; i++) { + const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); + sphere.castShadow = true; + sphere.receiveShadow = true; + + scene.add(sphere); + + spheres.push({ + mesh: sphere, + collider: new THREE.Sphere(new THREE.Vector3(0, -100, 0), SPHERE_RADIUS), + velocity: new THREE.Vector3(), + }); +} + +const worldOctree = new Octree(); + +const playerCollider = new Capsule(new THREE.Vector3(0, 0.35, 0), new THREE.Vector3(0, 1, 0), 0.35); + +const playerVelocity = new THREE.Vector3(); +const playerDirection = new THREE.Vector3(); + +let playerOnFloor = false; +let mouseTime = 0; + +const keyStates = {}; + +const vector1 = new THREE.Vector3(); +const vector2 = new THREE.Vector3(); +const vector3 = new THREE.Vector3(); + +document.addEventListener('keydown', event => { + keyStates[event.code] = true; +}); + +document.addEventListener('keyup', event => { + keyStates[event.code] = false; +}); + +container.addEventListener('mousedown', () => { + document.body.requestPointerLock(); + + mouseTime = performance.now(); +}); + +document.addEventListener('mouseup', () => { + if (document.pointerLockElement !== null) throwBall(); +}); + +document.body.addEventListener('mousemove', event => { + if (document.pointerLockElement === document.body) { + camera.rotation.y -= event.movementX / 500; + camera.rotation.x -= event.movementY / 500; + } +}); + +window.addEventListener('resize', onWindowResize); + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function throwBall() { + const sphere = spheres[sphereIdx]; + + camera.getWorldDirection(playerDirection); + + sphere.collider.center.copy(playerCollider.end).addScaledVector(playerDirection, playerCollider.radius * 1.5); + + // throw the ball with more force if we hold the button longer, and if we move forward + + const impulse = 15 + 30 * (1 - Math.exp((mouseTime - performance.now()) * 0.001)); + + sphere.velocity.copy(playerDirection).multiplyScalar(impulse); + sphere.velocity.addScaledVector(playerVelocity, 2); + + sphereIdx = (sphereIdx + 1) % spheres.length; +} + +function playerCollisions() { + const result = worldOctree.capsuleIntersect(playerCollider); + + playerOnFloor = false; + + if (result) { + playerOnFloor = result.normal.y > 0; + + if (!playerOnFloor) { + playerVelocity.addScaledVector(result.normal, -result.normal.dot(playerVelocity)); + } + + if (result.depth >= 1e-10) { + playerCollider.translate(result.normal.multiplyScalar(result.depth)); + } + } +} + +function updatePlayer(deltaTime) { + let damping = Math.exp(-4 * deltaTime) - 1; + + if (!playerOnFloor) { + playerVelocity.y -= GRAVITY * deltaTime; + + // small air resistance + damping *= 0.1; + } + + playerVelocity.addScaledVector(playerVelocity, damping); + + const deltaPosition = playerVelocity.clone().multiplyScalar(deltaTime); + playerCollider.translate(deltaPosition); + + playerCollisions(); + + camera.position.copy(playerCollider.end); +} + +function playerSphereCollision(sphere) { + const center = vector1.addVectors(playerCollider.start, playerCollider.end).multiplyScalar(0.5); + + const sphere_center = sphere.collider.center; + + const r = playerCollider.radius + sphere.collider.radius; + const r2 = r * r; + + // approximation: player = 3 spheres + + for (const point of [playerCollider.start, playerCollider.end, center]) { + const d2 = point.distanceToSquared(sphere_center); + + if (d2 < r2) { + const normal = vector1.subVectors(point, sphere_center).normalize(); + const v1 = vector2.copy(normal).multiplyScalar(normal.dot(playerVelocity)); + const v2 = vector3.copy(normal).multiplyScalar(normal.dot(sphere.velocity)); + + playerVelocity.add(v2).sub(v1); + sphere.velocity.add(v1).sub(v2); + + const d = (r - Math.sqrt(d2)) / 2; + sphere_center.addScaledVector(normal, -d); + } + } +} + +function spheresCollisions() { + for (let i = 0, length = spheres.length; i < length; i++) { + const s1 = spheres[i]; + + for (let j = i + 1; j < length; j++) { + const s2 = spheres[j]; + + const d2 = s1.collider.center.distanceToSquared(s2.collider.center); + const r = s1.collider.radius + s2.collider.radius; + const r2 = r * r; + + if (d2 < r2) { + const normal = vector1.subVectors(s1.collider.center, s2.collider.center).normalize(); + const v1 = vector2.copy(normal).multiplyScalar(normal.dot(s1.velocity)); + const v2 = vector3.copy(normal).multiplyScalar(normal.dot(s2.velocity)); + + s1.velocity.add(v2).sub(v1); + s2.velocity.add(v1).sub(v2); + + const d = (r - Math.sqrt(d2)) / 2; + + s1.collider.center.addScaledVector(normal, d); + s2.collider.center.addScaledVector(normal, -d); + } + } + } +} + +function updateSpheres(deltaTime) { + spheres.forEach(sphere => { + sphere.collider.center.addScaledVector(sphere.velocity, deltaTime); + + const result = worldOctree.sphereIntersect(sphere.collider); + + if (result) { + sphere.velocity.addScaledVector(result.normal, -result.normal.dot(sphere.velocity) * 1.5); + sphere.collider.center.add(result.normal.multiplyScalar(result.depth)); + } else { + sphere.velocity.y -= GRAVITY * deltaTime; + } + + const damping = Math.exp(-1.5 * deltaTime) - 1; + sphere.velocity.addScaledVector(sphere.velocity, damping); + + playerSphereCollision(sphere); + }); + + spheresCollisions(); + + for (const sphere of spheres) { + sphere.mesh.position.copy(sphere.collider.center); + } +} + +function getForwardVector() { + camera.getWorldDirection(playerDirection); + playerDirection.y = 0; + playerDirection.normalize(); + + return playerDirection; +} + +function getSideVector() { + camera.getWorldDirection(playerDirection); + playerDirection.y = 0; + playerDirection.normalize(); + playerDirection.cross(camera.up); + + return playerDirection; +} + +function controls(deltaTime) { + // gives a bit of air control + const speedDelta = deltaTime * (playerOnFloor ? 25 : 8); + + if (keyStates['KeyW']) { + playerVelocity.add(getForwardVector().multiplyScalar(speedDelta)); + } + + if (keyStates['KeyS']) { + playerVelocity.add(getForwardVector().multiplyScalar(-speedDelta)); + } + + if (keyStates['KeyA']) { + playerVelocity.add(getSideVector().multiplyScalar(-speedDelta)); + } + + if (keyStates['KeyD']) { + playerVelocity.add(getSideVector().multiplyScalar(speedDelta)); + } + + if (playerOnFloor) { + if (keyStates['Space']) { + playerVelocity.y = 15; + } + } +} + +const loader = new GLTFLoader().setPath('./models/gltf/'); + +loader.load('collision-world.glb', gltf => { + scene.add(gltf.scene); + + worldOctree.fromGraphNode(gltf.scene); + + gltf.scene.traverse(child => { + if (child.isMesh) { + child.castShadow = true; + child.receiveShadow = true; + + if (child.material.map) { + child.material.map.anisotropy = 4; + } + } + }); + + const helper = new OctreeHelper(worldOctree); + helper.visible = false; + scene.add(helper); + + const gui = new GUI({ width: 200 }); + gui.add({ debug: false }, 'debug').onChange(function (value) { + helper.visible = value; + }); +}); + +function teleportPlayerIfOob() { + if (camera.position.y <= -25) { + playerCollider.start.set(0, 0.35, 0); + playerCollider.end.set(0, 1, 0); + playerCollider.radius = 0.35; + camera.position.copy(playerCollider.end); + camera.rotation.set(0, 0, 0); + } +} + +function animate() { + const deltaTime = Math.min(0.05, clock.getDelta()) / STEPS_PER_FRAME; + + // we look for collisions in substeps to mitigate the risk of + // an object traversing another too quickly for detection. + + for (let i = 0; i < STEPS_PER_FRAME; i++) { + controls(deltaTime); + + updatePlayer(deltaTime); + + updateSpheres(deltaTime); + + teleportPlayerIfOob(); + } + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/misc_animation_groups.ts b/examples-testing/examples/misc_animation_groups.ts new file mode 100644 index 000000000..33fc41997 --- /dev/null +++ b/examples-testing/examples/misc_animation_groups.ts @@ -0,0 +1,125 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let stats, clock; +let scene, camera, renderer, mixer; + +init(); + +function init() { + scene = new THREE.Scene(); + + // + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(50, 50, 100); + camera.lookAt(scene.position); + + // all objects of this animation group share a common animation state + + const animationGroup = new THREE.AnimationObjectGroup(); + + // + + const geometry = new THREE.BoxGeometry(5, 5, 5); + const material = new THREE.MeshBasicMaterial({ transparent: true }); + + // + + for (let i = 0; i < 5; i++) { + for (let j = 0; j < 5; j++) { + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.x = 32 - 16 * i; + mesh.position.y = 0; + mesh.position.z = 32 - 16 * j; + + scene.add(mesh); + animationGroup.add(mesh); + } + } + + // create some keyframe tracks + + const xAxis = new THREE.Vector3(1, 0, 0); + const qInitial = new THREE.Quaternion().setFromAxisAngle(xAxis, 0); + const qFinal = new THREE.Quaternion().setFromAxisAngle(xAxis, Math.PI); + const quaternionKF = new THREE.QuaternionKeyframeTrack( + '.quaternion', + [0, 1, 2], + [ + qInitial.x, + qInitial.y, + qInitial.z, + qInitial.w, + qFinal.x, + qFinal.y, + qFinal.z, + qFinal.w, + qInitial.x, + qInitial.y, + qInitial.z, + qInitial.w, + ], + ); + + const colorKF = new THREE.ColorKeyframeTrack( + '.material.color', + [0, 1, 2], + [1, 0, 0, 0, 1, 0, 0, 0, 1], + THREE.InterpolateDiscrete, + ); + const opacityKF = new THREE.NumberKeyframeTrack('.material.opacity', [0, 1, 2], [1, 0, 1]); + + // create clip + + const clip = new THREE.AnimationClip('default', 3, [quaternionKF, colorKF, opacityKF]); + + // apply the animation group to the mixer as the root object + + mixer = new THREE.AnimationMixer(animationGroup); + + const clipAction = mixer.clipAction(clip); + clipAction.play(); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + clock = new THREE.Clock(); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const delta = clock.getDelta(); + + if (mixer) { + mixer.update(delta); + } + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/misc_animation_keys.ts b/examples-testing/examples/misc_animation_keys.ts new file mode 100644 index 000000000..e2f141f91 --- /dev/null +++ b/examples-testing/examples/misc_animation_keys.ts @@ -0,0 +1,129 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let stats, clock; +let scene, camera, renderer, mixer; + +init(); + +function init() { + scene = new THREE.Scene(); + + // + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(25, 25, 50); + camera.lookAt(scene.position); + + // + + const axesHelper = new THREE.AxesHelper(10); + scene.add(axesHelper); + + // + + const geometry = new THREE.BoxGeometry(5, 5, 5); + const material = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true }); + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // create a keyframe track (i.e. a timed sequence of keyframes) for each animated property + // Note: the keyframe track type should correspond to the type of the property being animated + + // POSITION + const positionKF = new THREE.VectorKeyframeTrack('.position', [0, 1, 2], [0, 0, 0, 30, 0, 0, 0, 0, 0]); + + // SCALE + const scaleKF = new THREE.VectorKeyframeTrack('.scale', [0, 1, 2], [1, 1, 1, 2, 2, 2, 1, 1, 1]); + + // ROTATION + // Rotation should be performed using quaternions, using a THREE.QuaternionKeyframeTrack + // Interpolating Euler angles (.rotation property) can be problematic and is currently not supported + + // set up rotation about x axis + const xAxis = new THREE.Vector3(1, 0, 0); + + const qInitial = new THREE.Quaternion().setFromAxisAngle(xAxis, 0); + const qFinal = new THREE.Quaternion().setFromAxisAngle(xAxis, Math.PI); + const quaternionKF = new THREE.QuaternionKeyframeTrack( + '.quaternion', + [0, 1, 2], + [ + qInitial.x, + qInitial.y, + qInitial.z, + qInitial.w, + qFinal.x, + qFinal.y, + qFinal.z, + qFinal.w, + qInitial.x, + qInitial.y, + qInitial.z, + qInitial.w, + ], + ); + + // COLOR + const colorKF = new THREE.ColorKeyframeTrack( + '.material.color', + [0, 1, 2], + [1, 0, 0, 0, 1, 0, 0, 0, 1], + THREE.InterpolateDiscrete, + ); + + // OPACITY + const opacityKF = new THREE.NumberKeyframeTrack('.material.opacity', [0, 1, 2], [1, 0, 1]); + + // create an animation sequence with the tracks + // If a negative time value is passed, the duration will be calculated from the times of the passed tracks array + const clip = new THREE.AnimationClip('Action', 3, [scaleKF, positionKF, quaternionKF, colorKF, opacityKF]); + + // setup the THREE.AnimationMixer + mixer = new THREE.AnimationMixer(mesh); + + // create a ClipAction and set it to play + const clipAction = mixer.clipAction(clip); + clipAction.play(); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + clock = new THREE.Clock(); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const delta = clock.getDelta(); + + if (mixer) { + mixer.update(delta); + } + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/misc_boxselection.ts b/examples-testing/examples/misc_boxselection.ts new file mode 100644 index 000000000..e7079c405 --- /dev/null +++ b/examples-testing/examples/misc_boxselection.ts @@ -0,0 +1,137 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { SelectionBox } from 'three/addons/interactive/SelectionBox.js'; +import { SelectionHelper } from 'three/addons/interactive/SelectionHelper.js'; + +let container, stats; +let camera, scene, renderer; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 500); + camera.position.z = 50; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + scene.add(new THREE.AmbientLight(0xaaaaaa)); + + const light = new THREE.SpotLight(0xffffff, 10000); + light.position.set(0, 25, 50); + light.angle = Math.PI / 5; + + light.castShadow = true; + light.shadow.camera.near = 10; + light.shadow.camera.far = 100; + light.shadow.mapSize.width = 1024; + light.shadow.mapSize.height = 1024; + + scene.add(light); + + const geometry = new THREE.BoxGeometry(); + + for (let i = 0; i < 200; i++) { + const object = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ color: Math.random() * 0xffffff })); + + object.position.x = Math.random() * 80 - 40; + object.position.y = Math.random() * 45 - 25; + object.position.z = Math.random() * 45 - 25; + + object.rotation.x = Math.random() * 2 * Math.PI; + object.rotation.y = Math.random() * 2 * Math.PI; + object.rotation.z = Math.random() * 2 * Math.PI; + + object.scale.x = Math.random() * 2 + 1; + object.scale.y = Math.random() * 2 + 1; + object.scale.z = Math.random() * 2 + 1; + + object.castShadow = true; + object.receiveShadow = true; + + scene.add(object); + } + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.PCFShadowMap; + + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + renderer.render(scene, camera); + + stats.update(); +} + +const selectionBox = new SelectionBox(camera, scene); +const helper = new SelectionHelper(renderer, 'selectBox'); + +document.addEventListener('pointerdown', function (event) { + for (const item of selectionBox.collection) { + item.material.emissive.set(0x000000); + } + + selectionBox.startPoint.set( + (event.clientX / window.innerWidth) * 2 - 1, + -(event.clientY / window.innerHeight) * 2 + 1, + 0.5, + ); +}); + +document.addEventListener('pointermove', function (event) { + if (helper.isDown) { + for (let i = 0; i < selectionBox.collection.length; i++) { + selectionBox.collection[i].material.emissive.set(0x000000); + } + + selectionBox.endPoint.set( + (event.clientX / window.innerWidth) * 2 - 1, + -(event.clientY / window.innerHeight) * 2 + 1, + 0.5, + ); + + const allSelected = selectionBox.select(); + + for (let i = 0; i < allSelected.length; i++) { + allSelected[i].material.emissive.set(0xffffff); + } + } +}); + +document.addEventListener('pointerup', function (event) { + selectionBox.endPoint.set( + (event.clientX / window.innerWidth) * 2 - 1, + -(event.clientY / window.innerHeight) * 2 + 1, + 0.5, + ); + + const allSelected = selectionBox.select(); + + for (let i = 0; i < allSelected.length; i++) { + allSelected[i].material.emissive.set(0xffffff); + } +}); diff --git a/examples-testing/examples/misc_controls_arcball.ts b/examples-testing/examples/misc_controls_arcball.ts new file mode 100644 index 000000000..fbef33189 --- /dev/null +++ b/examples-testing/examples/misc_controls_arcball.ts @@ -0,0 +1,210 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { ArcballControls } from 'three/addons/controls/ArcballControls.js'; + +import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +const cameras = ['Orthographic', 'Perspective']; +const cameraType = { type: 'Perspective' }; + +const perspectiveDistance = 2.5; +const orthographicDistance = 120; +let camera, controls, scene, renderer, gui; +let folderOptions, folderAnimations; + +const arcballGui = { + gizmoVisible: true, + + setArcballControls: function () { + controls = new ArcballControls(camera, renderer.domElement, scene); + controls.addEventListener('change', render); + + this.gizmoVisible = true; + + this.populateGui(); + }, + + populateGui: function () { + folderOptions.add(controls, 'enabled').name('Enable controls'); + folderOptions.add(controls, 'enableGrid').name('Enable Grid'); + folderOptions.add(controls, 'enableRotate').name('Enable rotate'); + folderOptions.add(controls, 'enablePan').name('Enable pan'); + folderOptions.add(controls, 'enableZoom').name('Enable zoom'); + folderOptions.add(controls, 'cursorZoom').name('Cursor zoom'); + folderOptions.add(controls, 'adjustNearFar').name('adjust near/far'); + folderOptions.add(controls, 'scaleFactor', 1.1, 10, 0.1).name('Scale factor'); + folderOptions.add(controls, 'minDistance', 0, 50, 0.5).name('Min distance'); + folderOptions.add(controls, 'maxDistance', 0, 50, 0.5).name('Max distance'); + folderOptions.add(controls, 'minZoom', 0, 50, 0.5).name('Min zoom'); + folderOptions.add(controls, 'maxZoom', 0, 50, 0.5).name('Max zoom'); + folderOptions + .add(arcballGui, 'gizmoVisible') + .name('Show gizmos') + .onChange(function () { + controls.setGizmosVisible(arcballGui.gizmoVisible); + }); + folderOptions.add(controls, 'copyState').name('Copy state(ctrl+c)'); + folderOptions.add(controls, 'pasteState').name('Paste state(ctrl+v)'); + folderOptions.add(controls, 'reset').name('Reset'); + folderAnimations.add(controls, 'enableAnimations').name('Enable anim.'); + folderAnimations.add(controls, 'dampingFactor', 0, 100, 1).name('Damping'); + folderAnimations.add(controls, 'wMax', 0, 100, 1).name('Angular spd'); + }, +}; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.toneMapping = THREE.ReinhardToneMapping; + renderer.toneMappingExposure = 3; + renderer.domElement.style.background = 'linear-gradient( 180deg, rgba( 0,0,0,1 ) 0%, rgba( 128,128,255,1 ) 100% )'; + container.appendChild(renderer.domElement); + + // + + scene = new THREE.Scene(); + + camera = makePerspectiveCamera(); + camera.position.set(0, 0, perspectiveDistance); + + const material = new THREE.MeshStandardMaterial(); + + new OBJLoader().setPath('models/obj/cerberus/').load('Cerberus.obj', function (group) { + const textureLoader = new THREE.TextureLoader().setPath('models/obj/cerberus/'); + + material.roughness = 1; + material.metalness = 1; + + const diffuseMap = textureLoader.load('Cerberus_A.jpg', render); + diffuseMap.colorSpace = THREE.SRGBColorSpace; + material.map = diffuseMap; + + material.metalnessMap = material.roughnessMap = textureLoader.load('Cerberus_RM.jpg', render); + material.normalMap = textureLoader.load('Cerberus_N.jpg', render); + + material.map.wrapS = THREE.RepeatWrapping; + material.roughnessMap.wrapS = THREE.RepeatWrapping; + material.metalnessMap.wrapS = THREE.RepeatWrapping; + material.normalMap.wrapS = THREE.RepeatWrapping; + + group.traverse(function (child) { + if (child.isMesh) { + child.material = material; + } + }); + + group.rotation.y = Math.PI / 2; + group.position.x += 0.25; + scene.add(group); + render(); + + new RGBELoader().setPath('textures/equirectangular/').load('venice_sunset_1k.hdr', function (hdrEquirect) { + hdrEquirect.mapping = THREE.EquirectangularReflectionMapping; + + scene.environment = hdrEquirect; + + render(); + }); + + window.addEventListener('keydown', onKeyDown); + window.addEventListener('resize', onWindowResize); + + // + + gui = new GUI(); + gui.add(cameraType, 'type', cameras) + .name('Choose Camera') + .onChange(function () { + setCamera(cameraType.type); + }); + + folderOptions = gui.addFolder('Arcball parameters'); + folderAnimations = folderOptions.addFolder('Animations'); + + arcballGui.setArcballControls(); + + render(); + }); +} + +function makeOrthographicCamera() { + const halfFovV = THREE.MathUtils.DEG2RAD * 45 * 0.5; + const halfFovH = Math.atan((window.innerWidth / window.innerHeight) * Math.tan(halfFovV)); + + const halfW = perspectiveDistance * Math.tan(halfFovH); + const halfH = perspectiveDistance * Math.tan(halfFovV); + const near = 0.01; + const far = 2000; + const newCamera = new THREE.OrthographicCamera(-halfW, halfW, halfH, -halfH, near, far); + return newCamera; +} + +function makePerspectiveCamera() { + const fov = 45; + const aspect = window.innerWidth / window.innerHeight; + const near = 0.01; + const far = 2000; + const newCamera = new THREE.PerspectiveCamera(fov, aspect, near, far); + return newCamera; +} + +function onWindowResize() { + if (camera.type == 'OrthographicCamera') { + const halfFovV = THREE.MathUtils.DEG2RAD * 45 * 0.5; + const halfFovH = Math.atan((window.innerWidth / window.innerHeight) * Math.tan(halfFovV)); + + const halfW = perspectiveDistance * Math.tan(halfFovH); + const halfH = perspectiveDistance * Math.tan(halfFovV); + camera.left = -halfW; + camera.right = halfW; + camera.top = halfH; + camera.bottom = -halfH; + } else if (camera.type == 'PerspectiveCamera') { + camera.aspect = window.innerWidth / window.innerHeight; + } + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, camera); +} + +function onKeyDown(event) { + if (event.key === 'c') { + if (event.ctrlKey || event.metaKey) { + controls.copyState(); + } + } else if (event.key === 'v') { + if (event.ctrlKey || event.metaKey) { + controls.pasteState(); + } + } +} + +function setCamera(type) { + if (type == 'Orthographic') { + camera = makeOrthographicCamera(); + camera.position.set(0, 0, orthographicDistance); + } else if (type == 'Perspective') { + camera = makePerspectiveCamera(); + camera.position.set(0, 0, perspectiveDistance); + } + + controls.setCamera(camera); + + render(); +} diff --git a/examples-testing/examples/misc_controls_drag.ts b/examples-testing/examples/misc_controls_drag.ts new file mode 100644 index 000000000..b12b0421e --- /dev/null +++ b/examples-testing/examples/misc_controls_drag.ts @@ -0,0 +1,153 @@ +import * as THREE from 'three'; + +import { DragControls } from 'three/addons/controls/DragControls.js'; + +let container; +let camera, scene, renderer; +let controls, group; +let enableSelection = false; + +const objects = []; + +const mouse = new THREE.Vector2(), + raycaster = new THREE.Raycaster(); + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 500); + camera.position.z = 25; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + scene.add(new THREE.AmbientLight(0xaaaaaa)); + + const light = new THREE.SpotLight(0xffffff, 10000); + light.position.set(0, 25, 50); + light.angle = Math.PI / 9; + + light.castShadow = true; + light.shadow.camera.near = 10; + light.shadow.camera.far = 100; + light.shadow.mapSize.width = 1024; + light.shadow.mapSize.height = 1024; + + scene.add(light); + + group = new THREE.Group(); + scene.add(group); + + const geometry = new THREE.BoxGeometry(); + + for (let i = 0; i < 200; i++) { + const object = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ color: Math.random() * 0xffffff })); + + object.position.x = Math.random() * 30 - 15; + object.position.y = Math.random() * 15 - 7.5; + object.position.z = Math.random() * 20 - 10; + + object.rotation.x = Math.random() * 2 * Math.PI; + object.rotation.y = Math.random() * 2 * Math.PI; + object.rotation.z = Math.random() * 2 * Math.PI; + + object.scale.x = Math.random() * 2 + 1; + object.scale.y = Math.random() * 2 + 1; + object.scale.z = Math.random() * 2 + 1; + + object.castShadow = true; + object.receiveShadow = true; + + scene.add(object); + + objects.push(object); + } + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.PCFShadowMap; + + container.appendChild(renderer.domElement); + + controls = new DragControls([...objects], camera, renderer.domElement); + controls.rotateSpeed = 2; + controls.addEventListener('drag', render); + + // + + window.addEventListener('resize', onWindowResize); + + document.addEventListener('click', onClick); + window.addEventListener('keydown', onKeyDown); + window.addEventListener('keyup', onKeyUp); + + render(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function onKeyDown(event) { + enableSelection = event.keyCode === 16 ? true : false; + + if (event.keyCode === 77) { + controls.touches.ONE = controls.touches.ONE === THREE.TOUCH.PAN ? THREE.TOUCH.ROTATE : THREE.TOUCH.PAN; + } +} + +function onKeyUp() { + enableSelection = false; +} + +function onClick(event) { + event.preventDefault(); + + if (enableSelection === true) { + const draggableObjects = controls.objects; + draggableObjects.length = 0; + + mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; + + raycaster.setFromCamera(mouse, camera); + + const intersections = raycaster.intersectObjects(objects, true); + + if (intersections.length > 0) { + const object = intersections[0].object; + + if (group.children.includes(object) === true) { + object.material.emissive.set(0x000000); + scene.attach(object); + } else { + object.material.emissive.set(0xaaaaaa); + group.attach(object); + } + + controls.transformGroup = true; + draggableObjects.push(group); + } + + if (group.children.length === 0) { + controls.transformGroup = false; + draggableObjects.push(...objects); + } + } + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/misc_controls_fly.ts b/examples-testing/examples/misc_controls_fly.ts new file mode 100644 index 000000000..4e58a36ec --- /dev/null +++ b/examples-testing/examples/misc_controls_fly.ts @@ -0,0 +1,214 @@ +import * as THREE from 'three'; +import { pass } from 'three/tsl'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { FlyControls } from 'three/addons/controls/FlyControls.js'; + +const radius = 6371; +const tilt = 0.41; +const rotationSpeed = 0.02; + +const cloudsScale = 1.005; +const moonScale = 0.23; + +const MARGIN = 0; +let SCREEN_HEIGHT = window.innerHeight - MARGIN * 2; +let SCREEN_WIDTH = window.innerWidth; + +let camera, controls, scene, renderer, stats; +let geometry, meshPlanet, meshClouds, meshMoon; +let dirLight; + +let postProcessing; + +const textureLoader = new THREE.TextureLoader(); + +let d, dPlanet, dMoon; +const dMoonVec = new THREE.Vector3(); + +const clock = new THREE.Clock(); + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(25, SCREEN_WIDTH / SCREEN_HEIGHT, 50, 1e7); + camera.position.z = radius * 5; + + scene = new THREE.Scene(); + scene.fog = new THREE.FogExp2(0x000000, 0.00000025); + + dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.position.set(-1, 0, 1).normalize(); + scene.add(dirLight); + + const materialNormalMap = new THREE.MeshPhongMaterial({ + specular: 0x7c7c7c, + shininess: 15, + map: textureLoader.load('textures/planets/earth_atmos_2048.jpg'), + specularMap: textureLoader.load('textures/planets/earth_specular_2048.jpg'), + normalMap: textureLoader.load('textures/planets/earth_normal_2048.jpg'), + + // y scale is negated to compensate for normal map handedness. + normalScale: new THREE.Vector2(0.85, -0.85), + }); + materialNormalMap.map.colorSpace = THREE.SRGBColorSpace; + + // planet + + geometry = new THREE.SphereGeometry(radius, 100, 50); + + meshPlanet = new THREE.Mesh(geometry, materialNormalMap); + meshPlanet.rotation.y = 0; + meshPlanet.rotation.z = tilt; + scene.add(meshPlanet); + + // clouds + + const materialClouds = new THREE.MeshLambertMaterial({ + map: textureLoader.load('textures/planets/earth_clouds_1024.png'), + transparent: true, + }); + materialClouds.map.colorSpace = THREE.SRGBColorSpace; + + meshClouds = new THREE.Mesh(geometry, materialClouds); + meshClouds.scale.set(cloudsScale, cloudsScale, cloudsScale); + meshClouds.rotation.z = tilt; + scene.add(meshClouds); + + // moon + + const materialMoon = new THREE.MeshPhongMaterial({ + map: textureLoader.load('textures/planets/moon_1024.jpg'), + }); + materialMoon.map.colorSpace = THREE.SRGBColorSpace; + + meshMoon = new THREE.Mesh(geometry, materialMoon); + meshMoon.position.set(radius * 5, 0, 0); + meshMoon.scale.set(moonScale, moonScale, moonScale); + scene.add(meshMoon); + + // stars + + const r = radius, + starsGeometry = [new THREE.BufferGeometry(), new THREE.BufferGeometry()]; + + const vertices1 = []; + const vertices2 = []; + + const vertex = new THREE.Vector3(); + + for (let i = 0; i < 250; i++) { + vertex.x = Math.random() * 2 - 1; + vertex.y = Math.random() * 2 - 1; + vertex.z = Math.random() * 2 - 1; + vertex.multiplyScalar(r); + + vertices1.push(vertex.x, vertex.y, vertex.z); + } + + for (let i = 0; i < 1500; i++) { + vertex.x = Math.random() * 2 - 1; + vertex.y = Math.random() * 2 - 1; + vertex.z = Math.random() * 2 - 1; + vertex.multiplyScalar(r); + + vertices2.push(vertex.x, vertex.y, vertex.z); + } + + starsGeometry[0].setAttribute('position', new THREE.Float32BufferAttribute(vertices1, 3)); + starsGeometry[1].setAttribute('position', new THREE.Float32BufferAttribute(vertices2, 3)); + + const starsMaterials = [ + new THREE.PointsMaterial({ color: 0x9c9c9c }), + new THREE.PointsMaterial({ color: 0x838383 }), + new THREE.PointsMaterial({ color: 0x5a5a5a }), + ]; + + for (let i = 10; i < 30; i++) { + const stars = new THREE.Points(starsGeometry[i % 2], starsMaterials[i % 3]); + + stars.rotation.x = Math.random() * 6; + stars.rotation.y = Math.random() * 6; + stars.rotation.z = Math.random() * 6; + stars.scale.setScalar(i * 10); + + stars.matrixAutoUpdate = false; + stars.updateMatrix(); + + scene.add(stars); + } + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + controls = new FlyControls(camera, renderer.domElement); + + controls.movementSpeed = 1000; + controls.domElement = renderer.domElement; + controls.rollSpeed = Math.PI / 24; + controls.autoForward = false; + controls.dragToLook = false; + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); + + // postprocessing + + postProcessing = new THREE.PostProcessing(renderer); + + const scenePass = pass(scene, camera); + const scenePassColor = scenePass.getTextureNode(); + + postProcessing.outputNode = scenePassColor.film(); +} + +function onWindowResize() { + SCREEN_HEIGHT = window.innerHeight; + SCREEN_WIDTH = window.innerWidth; + + camera.aspect = SCREEN_WIDTH / SCREEN_HEIGHT; + camera.updateProjectionMatrix(); + + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + // rotate the planet and clouds + + const delta = clock.getDelta(); + + meshPlanet.rotation.y += rotationSpeed * delta; + meshClouds.rotation.y += 1.25 * rotationSpeed * delta; + + // slow down as we approach the surface + + dPlanet = camera.position.length(); + + dMoonVec.subVectors(camera.position, meshMoon.position); + dMoon = dMoonVec.length(); + + if (dMoon < dPlanet) { + d = dMoon - radius * moonScale * 1.01; + } else { + d = dPlanet - radius * 1.01; + } + + controls.movementSpeed = 0.33 * d; + controls.update(delta); + + postProcessing.render(); +} diff --git a/examples-testing/examples/misc_controls_map.ts b/examples-testing/examples/misc_controls_map.ts new file mode 100644 index 000000000..2f52190cf --- /dev/null +++ b/examples-testing/examples/misc_controls_map.ts @@ -0,0 +1,98 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { MapControls } from 'three/addons/controls/MapControls.js'; + +let camera, controls, scene, renderer; + +init(); +//render(); // remove when using animation loop + +function init() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xcccccc); + scene.fog = new THREE.FogExp2(0xcccccc, 0.002); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 200, -400); + + // controls + + controls = new MapControls(camera, renderer.domElement); + + //controls.addEventListener( 'change', render ); // call this only in static scenes (i.e., if there is no animation loop) + + controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled + controls.dampingFactor = 0.05; + + controls.screenSpacePanning = false; + + controls.minDistance = 100; + controls.maxDistance = 500; + + controls.maxPolarAngle = Math.PI / 2; + + // world + + const geometry = new THREE.BoxGeometry(); + geometry.translate(0, 0.5, 0); + const material = new THREE.MeshPhongMaterial({ color: 0xeeeeee, flatShading: true }); + + for (let i = 0; i < 500; i++) { + const mesh = new THREE.Mesh(geometry, material); + mesh.position.x = Math.random() * 1600 - 800; + mesh.position.y = 0; + mesh.position.z = Math.random() * 1600 - 800; + mesh.scale.x = 20; + mesh.scale.y = Math.random() * 80 + 10; + mesh.scale.z = 20; + mesh.updateMatrix(); + mesh.matrixAutoUpdate = false; + scene.add(mesh); + } + + // lights + + const dirLight1 = new THREE.DirectionalLight(0xffffff, 3); + dirLight1.position.set(1, 1, 1); + scene.add(dirLight1); + + const dirLight2 = new THREE.DirectionalLight(0x002288, 3); + dirLight2.position.set(-1, -1, -1); + scene.add(dirLight2); + + const ambientLight = new THREE.AmbientLight(0x555555); + scene.add(ambientLight); + + // + + window.addEventListener('resize', onWindowResize); + + const gui = new GUI(); + gui.add(controls, 'zoomToCursor'); + gui.add(controls, 'screenSpacePanning'); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(); // only required if controls.enableDamping = true, or if controls.autoRotate = true + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/misc_controls_orbit.ts b/examples-testing/examples/misc_controls_orbit.ts new file mode 100644 index 000000000..186e216cb --- /dev/null +++ b/examples-testing/examples/misc_controls_orbit.ts @@ -0,0 +1,89 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, controls, scene, renderer; + +init(); +//render(); // remove when using animation loop + +function init() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xcccccc); + scene.fog = new THREE.FogExp2(0xcccccc, 0.002); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(400, 200, 0); + + // controls + + controls = new OrbitControls(camera, renderer.domElement); + controls.listenToKeyEvents(window); // optional + + //controls.addEventListener( 'change', render ); // call this only in static scenes (i.e., if there is no animation loop) + + controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled + controls.dampingFactor = 0.05; + + controls.screenSpacePanning = false; + + controls.minDistance = 100; + controls.maxDistance = 500; + + controls.maxPolarAngle = Math.PI / 2; + + // world + + const geometry = new THREE.ConeGeometry(10, 30, 4, 1); + const material = new THREE.MeshPhongMaterial({ color: 0xffffff, flatShading: true }); + + for (let i = 0; i < 500; i++) { + const mesh = new THREE.Mesh(geometry, material); + mesh.position.x = Math.random() * 1600 - 800; + mesh.position.y = 0; + mesh.position.z = Math.random() * 1600 - 800; + mesh.updateMatrix(); + mesh.matrixAutoUpdate = false; + scene.add(mesh); + } + + // lights + + const dirLight1 = new THREE.DirectionalLight(0xffffff, 3); + dirLight1.position.set(1, 1, 1); + scene.add(dirLight1); + + const dirLight2 = new THREE.DirectionalLight(0x002288, 3); + dirLight2.position.set(-1, -1, -1); + scene.add(dirLight2); + + const ambientLight = new THREE.AmbientLight(0x555555); + scene.add(ambientLight); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(); // only required if controls.enableDamping = true, or if controls.autoRotate = true + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/misc_controls_pointerlock.ts b/examples-testing/examples/misc_controls_pointerlock.ts new file mode 100644 index 000000000..0b6fcc516 --- /dev/null +++ b/examples-testing/examples/misc_controls_pointerlock.ts @@ -0,0 +1,245 @@ +import * as THREE from 'three'; + +import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; + +let camera, scene, renderer, controls; + +const objects = []; + +let raycaster; + +let moveForward = false; +let moveBackward = false; +let moveLeft = false; +let moveRight = false; +let canJump = false; + +let prevTime = performance.now(); +const velocity = new THREE.Vector3(); +const direction = new THREE.Vector3(); +const vertex = new THREE.Vector3(); +const color = new THREE.Color(); + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.y = 10; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + scene.fog = new THREE.Fog(0xffffff, 0, 750); + + const light = new THREE.HemisphereLight(0xeeeeff, 0x777788, 2.5); + light.position.set(0.5, 1, 0.75); + scene.add(light); + + controls = new PointerLockControls(camera, document.body); + + const blocker = document.getElementById('blocker'); + const instructions = document.getElementById('instructions'); + + instructions.addEventListener('click', function () { + controls.lock(); + }); + + controls.addEventListener('lock', function () { + instructions.style.display = 'none'; + blocker.style.display = 'none'; + }); + + controls.addEventListener('unlock', function () { + blocker.style.display = 'block'; + instructions.style.display = ''; + }); + + scene.add(controls.object); + + const onKeyDown = function (event) { + switch (event.code) { + case 'ArrowUp': + case 'KeyW': + moveForward = true; + break; + + case 'ArrowLeft': + case 'KeyA': + moveLeft = true; + break; + + case 'ArrowDown': + case 'KeyS': + moveBackward = true; + break; + + case 'ArrowRight': + case 'KeyD': + moveRight = true; + break; + + case 'Space': + if (canJump === true) velocity.y += 350; + canJump = false; + break; + } + }; + + const onKeyUp = function (event) { + switch (event.code) { + case 'ArrowUp': + case 'KeyW': + moveForward = false; + break; + + case 'ArrowLeft': + case 'KeyA': + moveLeft = false; + break; + + case 'ArrowDown': + case 'KeyS': + moveBackward = false; + break; + + case 'ArrowRight': + case 'KeyD': + moveRight = false; + break; + } + }; + + document.addEventListener('keydown', onKeyDown); + document.addEventListener('keyup', onKeyUp); + + raycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(0, -1, 0), 0, 10); + + // floor + + let floorGeometry = new THREE.PlaneGeometry(2000, 2000, 100, 100); + floorGeometry.rotateX(-Math.PI / 2); + + // vertex displacement + + let position = floorGeometry.attributes.position; + + for (let i = 0, l = position.count; i < l; i++) { + vertex.fromBufferAttribute(position, i); + + vertex.x += Math.random() * 20 - 10; + vertex.y += Math.random() * 2; + vertex.z += Math.random() * 20 - 10; + + position.setXYZ(i, vertex.x, vertex.y, vertex.z); + } + + floorGeometry = floorGeometry.toNonIndexed(); // ensure each face has unique vertices + + position = floorGeometry.attributes.position; + const colorsFloor = []; + + for (let i = 0, l = position.count; i < l; i++) { + color.setHSL(Math.random() * 0.3 + 0.5, 0.75, Math.random() * 0.25 + 0.75, THREE.SRGBColorSpace); + colorsFloor.push(color.r, color.g, color.b); + } + + floorGeometry.setAttribute('color', new THREE.Float32BufferAttribute(colorsFloor, 3)); + + const floorMaterial = new THREE.MeshBasicMaterial({ vertexColors: true }); + + const floor = new THREE.Mesh(floorGeometry, floorMaterial); + scene.add(floor); + + // objects + + const boxGeometry = new THREE.BoxGeometry(20, 20, 20).toNonIndexed(); + + position = boxGeometry.attributes.position; + const colorsBox = []; + + for (let i = 0, l = position.count; i < l; i++) { + color.setHSL(Math.random() * 0.3 + 0.5, 0.75, Math.random() * 0.25 + 0.75, THREE.SRGBColorSpace); + colorsBox.push(color.r, color.g, color.b); + } + + boxGeometry.setAttribute('color', new THREE.Float32BufferAttribute(colorsBox, 3)); + + for (let i = 0; i < 500; i++) { + const boxMaterial = new THREE.MeshPhongMaterial({ specular: 0xffffff, flatShading: true, vertexColors: true }); + boxMaterial.color.setHSL(Math.random() * 0.2 + 0.5, 0.75, Math.random() * 0.25 + 0.75, THREE.SRGBColorSpace); + + const box = new THREE.Mesh(boxGeometry, boxMaterial); + box.position.x = Math.floor(Math.random() * 20 - 10) * 20; + box.position.y = Math.floor(Math.random() * 20) * 20 + 10; + box.position.z = Math.floor(Math.random() * 20 - 10) * 20; + + scene.add(box); + objects.push(box); + } + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const time = performance.now(); + + if (controls.isLocked === true) { + raycaster.ray.origin.copy(controls.object.position); + raycaster.ray.origin.y -= 10; + + const intersections = raycaster.intersectObjects(objects, false); + + const onObject = intersections.length > 0; + + const delta = (time - prevTime) / 1000; + + velocity.x -= velocity.x * 10.0 * delta; + velocity.z -= velocity.z * 10.0 * delta; + + velocity.y -= 9.8 * 100.0 * delta; // 100.0 = mass + + direction.z = Number(moveForward) - Number(moveBackward); + direction.x = Number(moveRight) - Number(moveLeft); + direction.normalize(); // this ensures consistent movements in all directions + + if (moveForward || moveBackward) velocity.z -= direction.z * 400.0 * delta; + if (moveLeft || moveRight) velocity.x -= direction.x * 400.0 * delta; + + if (onObject === true) { + velocity.y = Math.max(0, velocity.y); + canJump = true; + } + + controls.moveRight(-velocity.x * delta); + controls.moveForward(-velocity.z * delta); + + controls.object.position.y += velocity.y * delta; // new behavior + + if (controls.object.position.y < 10) { + velocity.y = 0; + controls.object.position.y = 10; + + canJump = true; + } + } + + prevTime = time; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/misc_controls_trackball.ts b/examples-testing/examples/misc_controls_trackball.ts new file mode 100644 index 000000000..b6479e9f6 --- /dev/null +++ b/examples-testing/examples/misc_controls_trackball.ts @@ -0,0 +1,134 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { TrackballControls } from 'three/addons/controls/TrackballControls.js'; + +let perspectiveCamera, orthographicCamera, controls, scene, renderer, stats; + +const params = { + orthographicCamera: false, +}; + +const frustumSize = 400; + +init(); + +function init() { + const aspect = window.innerWidth / window.innerHeight; + + perspectiveCamera = new THREE.PerspectiveCamera(60, aspect, 1, 1000); + perspectiveCamera.position.z = 500; + + orthographicCamera = new THREE.OrthographicCamera( + (frustumSize * aspect) / -2, + (frustumSize * aspect) / 2, + frustumSize / 2, + frustumSize / -2, + 1, + 1000, + ); + orthographicCamera.position.z = 500; + + // world + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xcccccc); + scene.fog = new THREE.FogExp2(0xcccccc, 0.002); + + const geometry = new THREE.ConeGeometry(10, 30, 4, 1); + const material = new THREE.MeshPhongMaterial({ color: 0xffffff, flatShading: true }); + + for (let i = 0; i < 500; i++) { + const mesh = new THREE.Mesh(geometry, material); + mesh.position.x = (Math.random() - 0.5) * 1000; + mesh.position.y = (Math.random() - 0.5) * 1000; + mesh.position.z = (Math.random() - 0.5) * 1000; + mesh.updateMatrix(); + mesh.matrixAutoUpdate = false; + scene.add(mesh); + } + + // lights + + const dirLight1 = new THREE.DirectionalLight(0xffffff, 3); + dirLight1.position.set(1, 1, 1); + scene.add(dirLight1); + + const dirLight2 = new THREE.DirectionalLight(0x002288, 3); + dirLight2.position.set(-1, -1, -1); + scene.add(dirLight2); + + const ambientLight = new THREE.AmbientLight(0x555555); + scene.add(ambientLight); + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + const gui = new GUI(); + gui.add(params, 'orthographicCamera') + .name('use orthographic') + .onChange(function (value) { + controls.dispose(); + + createControls(value ? orthographicCamera : perspectiveCamera); + }); + + // + + window.addEventListener('resize', onWindowResize); + + createControls(perspectiveCamera); +} + +function createControls(camera) { + controls = new TrackballControls(camera, renderer.domElement); + + controls.rotateSpeed = 1.0; + controls.zoomSpeed = 1.2; + controls.panSpeed = 0.8; + + controls.keys = ['KeyA', 'KeyS', 'KeyD']; +} + +function onWindowResize() { + const aspect = window.innerWidth / window.innerHeight; + + perspectiveCamera.aspect = aspect; + perspectiveCamera.updateProjectionMatrix(); + + orthographicCamera.left = (-frustumSize * aspect) / 2; + orthographicCamera.right = (frustumSize * aspect) / 2; + orthographicCamera.top = frustumSize / 2; + orthographicCamera.bottom = -frustumSize / 2; + orthographicCamera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + controls.handleResize(); +} + +function animate() { + controls.update(); + + render(); + + stats.update(); +} + +function render() { + const camera = params.orthographicCamera ? orthographicCamera : perspectiveCamera; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/misc_controls_transform.ts b/examples-testing/examples/misc_controls_transform.ts new file mode 100644 index 000000000..9d14bf7ee --- /dev/null +++ b/examples-testing/examples/misc_controls_transform.ts @@ -0,0 +1,181 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { TransformControls } from 'three/addons/controls/TransformControls.js'; + +let cameraPersp, cameraOrtho, currentCamera; +let scene, renderer, control, orbit; + +init(); +render(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + const aspect = window.innerWidth / window.innerHeight; + + const frustumSize = 5; + + cameraPersp = new THREE.PerspectiveCamera(50, aspect, 0.1, 100); + cameraOrtho = new THREE.OrthographicCamera( + -frustumSize * aspect, + frustumSize * aspect, + frustumSize, + -frustumSize, + 0.1, + 100, + ); + currentCamera = cameraPersp; + + currentCamera.position.set(5, 2.5, 5); + + scene = new THREE.Scene(); + scene.add(new THREE.GridHelper(5, 10, 0x888888, 0x444444)); + + const ambientLight = new THREE.AmbientLight(0xffffff); + scene.add(ambientLight); + + const light = new THREE.DirectionalLight(0xffffff, 4); + light.position.set(1, 1, 1); + scene.add(light); + + const texture = new THREE.TextureLoader().load('textures/crate.gif', render); + texture.colorSpace = THREE.SRGBColorSpace; + texture.anisotropy = renderer.capabilities.getMaxAnisotropy(); + + const geometry = new THREE.BoxGeometry(); + const material = new THREE.MeshLambertMaterial({ map: texture }); + + orbit = new OrbitControls(currentCamera, renderer.domElement); + orbit.update(); + orbit.addEventListener('change', render); + + control = new TransformControls(currentCamera, renderer.domElement); + control.addEventListener('change', render); + + control.addEventListener('dragging-changed', function (event) { + orbit.enabled = !event.value; + }); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + control.attach(mesh); + scene.add(control); + + window.addEventListener('resize', onWindowResize); + + window.addEventListener('keydown', function (event) { + switch (event.key) { + case 'q': + control.setSpace(control.space === 'local' ? 'world' : 'local'); + break; + + case 'Shift': + control.setTranslationSnap(1); + control.setRotationSnap(THREE.MathUtils.degToRad(15)); + control.setScaleSnap(0.25); + break; + + case 'w': + control.setMode('translate'); + break; + + case 'e': + control.setMode('rotate'); + break; + + case 'r': + control.setMode('scale'); + break; + + case 'c': + const position = currentCamera.position.clone(); + + currentCamera = currentCamera.isPerspectiveCamera ? cameraOrtho : cameraPersp; + currentCamera.position.copy(position); + + orbit.object = currentCamera; + control.camera = currentCamera; + + currentCamera.lookAt(orbit.target.x, orbit.target.y, orbit.target.z); + onWindowResize(); + break; + + case 'v': + const randomFoV = Math.random() + 0.1; + const randomZoom = Math.random() + 0.1; + + cameraPersp.fov = randomFoV * 160; + cameraOrtho.bottom = -randomFoV * 500; + cameraOrtho.top = randomFoV * 500; + + cameraPersp.zoom = randomZoom * 5; + cameraOrtho.zoom = randomZoom * 5; + onWindowResize(); + break; + + case '+': + case '=': + control.setSize(control.size + 0.1); + break; + + case '-': + case '_': + control.setSize(Math.max(control.size - 0.1, 0.1)); + break; + + case 'x': + control.showX = !control.showX; + break; + + case 'y': + control.showY = !control.showY; + break; + + case 'z': + control.showZ = !control.showZ; + break; + + case ' ': + control.enabled = !control.enabled; + break; + + case 'Escape': + control.reset(); + break; + } + }); + + window.addEventListener('keyup', function (event) { + switch (event.key) { + case 'Shift': + control.setTranslationSnap(null); + control.setRotationSnap(null); + control.setScaleSnap(null); + break; + } + }); +} + +function onWindowResize() { + const aspect = window.innerWidth / window.innerHeight; + + cameraPersp.aspect = aspect; + cameraPersp.updateProjectionMatrix(); + + cameraOrtho.left = cameraOrtho.bottom * aspect; + cameraOrtho.right = cameraOrtho.top * aspect; + cameraOrtho.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, currentCamera); +} diff --git a/examples-testing/examples/misc_exporter_draco.ts b/examples-testing/examples/misc_exporter_draco.ts new file mode 100644 index 000000000..40a62fb18 --- /dev/null +++ b/examples-testing/examples/misc_exporter_draco.ts @@ -0,0 +1,117 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { DRACOExporter } from 'three/addons/exporters/DRACOExporter.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let scene, camera, renderer, exporter, mesh; + +const params = { + export: exportFile, +}; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(4, 2, 4); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xa0a0a0); + scene.fog = new THREE.Fog(0xa0a0a0, 4, 20); + + exporter = new DRACOExporter(); + + // + + const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 3); + hemiLight.position.set(0, 20, 0); + scene.add(hemiLight); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 3); + directionalLight.position.set(0, 20, 10); + directionalLight.castShadow = true; + directionalLight.shadow.camera.top = 2; + directionalLight.shadow.camera.bottom = -2; + directionalLight.shadow.camera.left = -2; + directionalLight.shadow.camera.right = 2; + scene.add(directionalLight); + + // ground + + const ground = new THREE.Mesh( + new THREE.PlaneGeometry(40, 40), + new THREE.MeshPhongMaterial({ color: 0xbbbbbb, depthWrite: false }), + ); + ground.rotation.x = -Math.PI / 2; + ground.receiveShadow = true; + scene.add(ground); + + const grid = new THREE.GridHelper(40, 20, 0x000000, 0x000000); + grid.material.opacity = 0.2; + grid.material.transparent = true; + scene.add(grid); + + // export mesh + + const geometry = new THREE.TorusKnotGeometry(0.75, 0.2, 200, 30); + const material = new THREE.MeshPhongMaterial({ color: 0x00ff00 }); + mesh = new THREE.Mesh(geometry, material); + mesh.castShadow = true; + mesh.position.y = 1.5; + scene.add(mesh); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + document.body.appendChild(renderer.domElement); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 1.5, 0); + controls.update(); + + // + + window.addEventListener('resize', onWindowResize); + + const gui = new GUI(); + + gui.add(params, 'export').name('Export DRC'); + gui.open(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} + +function exportFile() { + const result = exporter.parse(mesh); + saveArrayBuffer(result, 'file.drc'); +} + +const link = document.createElement('a'); +link.style.display = 'none'; +document.body.appendChild(link); + +function save(blob, filename) { + link.href = URL.createObjectURL(blob); + link.download = filename; + link.click(); +} + +function saveArrayBuffer(buffer, filename) { + save(new Blob([buffer], { type: 'application/octet-stream' }), filename); +} diff --git a/examples-testing/examples/misc_exporter_exr.ts b/examples-testing/examples/misc_exporter_exr.ts new file mode 100644 index 000000000..c239a65fa --- /dev/null +++ b/examples-testing/examples/misc_exporter_exr.ts @@ -0,0 +1,158 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { EXRExporter, ZIP_COMPRESSION, ZIPS_COMPRESSION, NO_COMPRESSION } from 'three/addons/exporters/EXRExporter.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let scene, camera, renderer, exporter, mesh, controls, renderTarget, dataTexture; + +const params = { + target: 'pmrem', + type: 'HalfFloatType', + compression: 'ZIP', + export: exportFile, +}; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(10, 0, 0); + + scene = new THREE.Scene(); + + exporter = new EXRExporter(); + const rgbeloader = new RGBELoader(); + + // + + const pmremGenerator = new THREE.PMREMGenerator(renderer); + pmremGenerator.compileEquirectangularShader(); + + rgbeloader.load('textures/equirectangular/san_giuseppe_bridge_2k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + renderTarget = pmremGenerator.fromEquirectangular(texture); + scene.background = renderTarget.texture; + }); + + createDataTexture(); + + // + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.rotateSpeed = -0.25; // negative, to track mouse pointer + + // + + window.addEventListener('resize', onWindowResize); + + const gui = new GUI(); + + const input = gui.addFolder('Input'); + input.add(params, 'target').options(['pmrem', 'data-texture']).onChange(swapScene); + + const options = gui.addFolder('Output Options'); + options.add(params, 'type').options(['FloatType', 'HalfFloatType']); + options.add(params, 'compression').options(['ZIP', 'ZIPS', 'NONE']); + + gui.add(params, 'export').name('Export EXR'); + gui.open(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(); + renderer.render(scene, camera); +} + +function createDataTexture() { + const normal = new THREE.Vector3(); + const coord = new THREE.Vector2(); + const size = 800, + radius = 320, + factor = (Math.PI * 0.5) / radius; + const data = new Float32Array(4 * size * size); + + for (let i = 0; i < size; i++) { + for (let j = 0; j < size; j++) { + const idx = i * size * 4 + j * 4; + coord.set(j, i).subScalar(size / 2); + + if (coord.length() < radius) + normal.set(Math.sin(coord.x * factor), Math.sin(coord.y * factor), Math.cos(coord.x * factor)); + else normal.set(0, 0, 1); + + data[idx + 0] = 0.5 + 0.5 * normal.x; + data[idx + 1] = 0.5 + 0.5 * normal.y; + data[idx + 2] = 0.5 + 0.5 * normal.z; + data[idx + 3] = 1; + } + } + + dataTexture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat, THREE.FloatType); + dataTexture.needsUpdate = true; + + const material = new THREE.MeshBasicMaterial({ map: dataTexture }); + const quad = new THREE.PlaneGeometry(50, 50); + mesh = new THREE.Mesh(quad, material); + mesh.visible = false; + + scene.add(mesh); +} + +function swapScene() { + if (params.target == 'pmrem') { + camera.position.set(10, 0, 0); + controls.enabled = true; + scene.background = renderTarget.texture; + mesh.visible = false; + } else { + camera.position.set(0, 0, 70); + controls.enabled = false; + scene.background = new THREE.Color(0, 0, 0); + mesh.visible = true; + } +} + +function exportFile() { + let result, exportType, exportCompression; + + if (params.type == 'HalfFloatType') exportType = THREE.HalfFloatType; + else exportType = THREE.FloatType; + + if (params.compression == 'ZIP') exportCompression = ZIP_COMPRESSION; + else if (params.compression == 'ZIPS') exportCompression = ZIPS_COMPRESSION; + else exportCompression = NO_COMPRESSION; + + if (params.target == 'pmrem') + result = exporter.parse(renderer, renderTarget, { type: exportType, compression: exportCompression }); + else result = exporter.parse(dataTexture, { type: exportType, compression: exportCompression }); + + saveArrayBuffer(result, params.target + '.exr'); +} + +function saveArrayBuffer(buffer, filename) { + const blob = new Blob([buffer], { type: 'image/x-exr' }); + const link = document.createElement('a'); + + link.href = URL.createObjectURL(blob); + link.download = filename; + link.click(); +} diff --git a/examples-testing/examples/misc_exporter_gltf.ts b/examples-testing/examples/misc_exporter_gltf.ts new file mode 100644 index 000000000..e4172b852 --- /dev/null +++ b/examples-testing/examples/misc_exporter_gltf.ts @@ -0,0 +1,507 @@ +import * as THREE from 'three'; + +import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'; +import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +function exportGLTF(input) { + const gltfExporter = new GLTFExporter(); + + const options = { + trs: params.trs, + onlyVisible: params.onlyVisible, + binary: params.binary, + maxTextureSize: params.maxTextureSize, + }; + gltfExporter.parse( + input, + function (result) { + if (result instanceof ArrayBuffer) { + saveArrayBuffer(result, 'scene.glb'); + } else { + const output = JSON.stringify(result, null, 2); + console.log(output); + saveString(output, 'scene.gltf'); + } + }, + function (error) { + console.log('An error happened during parsing', error); + }, + options, + ); +} + +const link = document.createElement('a'); +link.style.display = 'none'; +document.body.appendChild(link); // Firefox workaround, see #6594 + +function save(blob, filename) { + link.href = URL.createObjectURL(blob); + link.download = filename; + link.click(); + + // URL.revokeObjectURL( url ); breaks Firefox... +} + +function saveString(text, filename) { + save(new Blob([text], { type: 'text/plain' }), filename); +} + +function saveArrayBuffer(buffer, filename) { + save(new Blob([buffer], { type: 'application/octet-stream' }), filename); +} + +let container; + +let camera, object, object2, material, geometry, scene1, scene2, renderer; +let gridHelper, sphere, model, coffeemat; + +const params = { + trs: false, + onlyVisible: true, + binary: false, + maxTextureSize: 4096, + exportScene1: exportScene1, + exportScenes: exportScenes, + exportSphere: exportSphere, + exportModel: exportModel, + exportObjects: exportObjects, + exportSceneObject: exportSceneObject, + exportCompressedObject: exportCompressedObject, +}; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + // Make linear gradient texture + + const data = new Uint8ClampedArray(100 * 100 * 4); + + for (let y = 0; y < 100; y++) { + for (let x = 0; x < 100; x++) { + const stride = 4 * (100 * y + x); + + data[stride] = Math.round((255 * y) / 99); + data[stride + 1] = Math.round(255 - (255 * y) / 99); + data[stride + 2] = 0; + data[stride + 3] = 255; + } + } + + const gradientTexture = new THREE.DataTexture(data, 100, 100, THREE.RGBAFormat); + gradientTexture.minFilter = THREE.LinearFilter; + gradientTexture.magFilter = THREE.LinearFilter; + gradientTexture.needsUpdate = true; + + scene1 = new THREE.Scene(); + scene1.name = 'Scene1'; + + // --------------------------------------------------------------------- + // Perspective Camera + // --------------------------------------------------------------------- + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000); + camera.position.set(600, 400, 0); + + camera.name = 'PerspectiveCamera'; + scene1.add(camera); + + // --------------------------------------------------------------------- + // Ambient light + // --------------------------------------------------------------------- + const ambientLight = new THREE.AmbientLight(0xcccccc); + ambientLight.name = 'AmbientLight'; + scene1.add(ambientLight); + + // --------------------------------------------------------------------- + // DirectLight + // --------------------------------------------------------------------- + const dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.target.position.set(0, 0, -1); + dirLight.add(dirLight.target); + dirLight.lookAt(-1, -1, 0); + dirLight.name = 'DirectionalLight'; + scene1.add(dirLight); + + // --------------------------------------------------------------------- + // Grid + // --------------------------------------------------------------------- + gridHelper = new THREE.GridHelper(2000, 20, 0xc1c1c1, 0x8d8d8d); + gridHelper.position.y = -50; + gridHelper.name = 'Grid'; + scene1.add(gridHelper); + + // --------------------------------------------------------------------- + // Axes + // --------------------------------------------------------------------- + const axes = new THREE.AxesHelper(500); + axes.name = 'AxesHelper'; + scene1.add(axes); + + // --------------------------------------------------------------------- + // Simple geometry with basic material + // --------------------------------------------------------------------- + // Icosahedron + const mapGrid = new THREE.TextureLoader().load('textures/uv_grid_opengl.jpg'); + mapGrid.wrapS = mapGrid.wrapT = THREE.RepeatWrapping; + mapGrid.colorSpace = THREE.SRGBColorSpace; + material = new THREE.MeshBasicMaterial({ + color: 0xffffff, + map: mapGrid, + }); + + object = new THREE.Mesh(new THREE.IcosahedronGeometry(75, 0), material); + object.position.set(-200, 0, 200); + object.name = 'Icosahedron'; + scene1.add(object); + + // Octahedron + material = new THREE.MeshBasicMaterial({ + color: 0x0000ff, + wireframe: true, + }); + object = new THREE.Mesh(new THREE.OctahedronGeometry(75, 1), material); + object.position.set(0, 0, 200); + object.name = 'Octahedron'; + scene1.add(object); + + // Tetrahedron + material = new THREE.MeshBasicMaterial({ + color: 0xff0000, + transparent: true, + opacity: 0.5, + }); + + object = new THREE.Mesh(new THREE.TetrahedronGeometry(75, 0), material); + object.position.set(200, 0, 200); + object.name = 'Tetrahedron'; + scene1.add(object); + + // --------------------------------------------------------------------- + // Buffered geometry primitives + // --------------------------------------------------------------------- + // Sphere + material = new THREE.MeshStandardMaterial({ + color: 0xffff00, + metalness: 0.5, + roughness: 1.0, + flatShading: true, + }); + material.map = gradientTexture; + material.bumpMap = mapGrid; + sphere = new THREE.Mesh(new THREE.SphereGeometry(70, 10, 10), material); + sphere.position.set(0, 0, 0); + sphere.name = 'Sphere'; + scene1.add(sphere); + + // Cylinder + material = new THREE.MeshStandardMaterial({ + color: 0xff00ff, + flatShading: true, + }); + object = new THREE.Mesh(new THREE.CylinderGeometry(10, 80, 100), material); + object.position.set(200, 0, 0); + object.name = 'Cylinder'; + scene1.add(object); + + // TorusKnot + material = new THREE.MeshStandardMaterial({ + color: 0xff0000, + roughness: 1, + }); + object = new THREE.Mesh(new THREE.TorusKnotGeometry(50, 15, 40, 10), material); + object.position.set(-200, 0, 0); + object.name = 'Cylinder'; + scene1.add(object); + + // --------------------------------------------------------------------- + // Hierarchy + // --------------------------------------------------------------------- + const mapWood = new THREE.TextureLoader().load('textures/hardwood2_diffuse.jpg'); + material = new THREE.MeshStandardMaterial({ map: mapWood, side: THREE.DoubleSide }); + + object = new THREE.Mesh(new THREE.BoxGeometry(40, 100, 100), material); + object.position.set(-200, 0, 400); + object.name = 'Cube'; + scene1.add(object); + + object2 = new THREE.Mesh(new THREE.BoxGeometry(40, 40, 40, 2, 2, 2), material); + object2.position.set(0, 0, 50); + object2.rotation.set(0, 45, 0); + object2.name = 'SubCube'; + object.add(object2); + + // --------------------------------------------------------------------- + // Groups + // --------------------------------------------------------------------- + const group1 = new THREE.Group(); + group1.name = 'Group'; + scene1.add(group1); + + const group2 = new THREE.Group(); + group2.name = 'subGroup'; + group2.position.set(0, 50, 0); + group1.add(group2); + + object2 = new THREE.Mesh(new THREE.BoxGeometry(30, 30, 30), material); + object2.name = 'Cube in group'; + object2.position.set(0, 0, 400); + group2.add(object2); + + // --------------------------------------------------------------------- + // THREE.Line Strip + // --------------------------------------------------------------------- + geometry = new THREE.BufferGeometry(); + let numPoints = 100; + let positions = new Float32Array(numPoints * 3); + + for (let i = 0; i < numPoints; i++) { + positions[i * 3] = i; + positions[i * 3 + 1] = Math.sin(i / 2) * 20; + positions[i * 3 + 2] = 0; + } + + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + object = new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0xffff00 })); + object.position.set(-50, 0, -200); + scene1.add(object); + + // --------------------------------------------------------------------- + // THREE.Line Loop + // --------------------------------------------------------------------- + geometry = new THREE.BufferGeometry(); + numPoints = 5; + const radius = 70; + positions = new Float32Array(numPoints * 3); + + for (let i = 0; i < numPoints; i++) { + const s = (i * Math.PI * 2) / numPoints; + positions[i * 3] = radius * Math.sin(s); + positions[i * 3 + 1] = radius * Math.cos(s); + positions[i * 3 + 2] = 0; + } + + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + object = new THREE.LineLoop(geometry, new THREE.LineBasicMaterial({ color: 0xffff00 })); + object.position.set(0, 0, -200); + + scene1.add(object); + + // --------------------------------------------------------------------- + // THREE.Points + // --------------------------------------------------------------------- + numPoints = 100; + const pointsArray = new Float32Array(numPoints * 3); + for (let i = 0; i < numPoints; i++) { + pointsArray[3 * i] = -50 + Math.random() * 100; + pointsArray[3 * i + 1] = Math.random() * 100; + pointsArray[3 * i + 2] = -50 + Math.random() * 100; + } + + const pointsGeo = new THREE.BufferGeometry(); + pointsGeo.setAttribute('position', new THREE.BufferAttribute(pointsArray, 3)); + + const pointsMaterial = new THREE.PointsMaterial({ color: 0xffff00, size: 5 }); + const pointCloud = new THREE.Points(pointsGeo, pointsMaterial); + pointCloud.name = 'Points'; + pointCloud.position.set(-200, 0, -200); + scene1.add(pointCloud); + + // --------------------------------------------------------------------- + // Ortho camera + // --------------------------------------------------------------------- + const cameraOrtho = new THREE.OrthographicCamera( + window.innerWidth / -2, + window.innerWidth / 2, + window.innerHeight / 2, + window.innerHeight / -2, + 0.1, + 10, + ); + scene1.add(cameraOrtho); + cameraOrtho.name = 'OrthographicCamera'; + + material = new THREE.MeshLambertMaterial({ + color: 0xffff00, + side: THREE.DoubleSide, + }); + + object = new THREE.Mesh(new THREE.CircleGeometry(50, 20, 0, Math.PI * 2), material); + object.position.set(200, 0, -400); + scene1.add(object); + + object = new THREE.Mesh(new THREE.RingGeometry(10, 50, 20, 5, 0, Math.PI * 2), material); + object.position.set(0, 0, -400); + scene1.add(object); + + object = new THREE.Mesh(new THREE.CylinderGeometry(25, 75, 100, 40, 5), material); + object.position.set(-200, 0, -400); + scene1.add(object); + + // + const points = []; + + for (let i = 0; i < 50; i++) { + points.push(new THREE.Vector2(Math.sin(i * 0.2) * Math.sin(i * 0.1) * 15 + 50, (i - 5) * 2)); + } + + object = new THREE.Mesh(new THREE.LatheGeometry(points, 20), material); + object.position.set(200, 0, 400); + scene1.add(object); + + // --------------------------------------------------------------------- + // Big red box hidden just for testing `onlyVisible` option + // --------------------------------------------------------------------- + material = new THREE.MeshBasicMaterial({ + color: 0xff0000, + }); + object = new THREE.Mesh(new THREE.BoxGeometry(200, 200, 200), material); + object.position.set(0, 0, 0); + object.name = 'CubeHidden'; + object.visible = false; + scene1.add(object); + + // --------------------------------------------------------------------- + // Model requiring KHR_mesh_quantization + // --------------------------------------------------------------------- + const loader = new GLTFLoader(); + loader.load('models/gltf/ShaderBall.glb', function (gltf) { + model = gltf.scene; + model.scale.setScalar(50); + model.position.set(200, -40, -200); + scene1.add(model); + }); + + // --------------------------------------------------------------------- + // Model requiring KHR_mesh_quantization + // --------------------------------------------------------------------- + + material = new THREE.MeshBasicMaterial({ + color: 0xffffff, + }); + object = new THREE.InstancedMesh(new THREE.BoxGeometry(10, 10, 10, 2, 2, 2), material, 50); + const matrix = new THREE.Matrix4(); + const color = new THREE.Color(); + for (let i = 0; i < 50; i++) { + matrix.setPosition(Math.random() * 100 - 50, Math.random() * 100 - 50, Math.random() * 100 - 50); + object.setMatrixAt(i, matrix); + object.setColorAt(i, color.setHSL(i / 50, 1, 0.5)); + } + + object.position.set(400, 0, 200); + scene1.add(object); + + // --------------------------------------------------------------------- + // 2nd THREE.Scene + // --------------------------------------------------------------------- + scene2 = new THREE.Scene(); + object = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 100), material); + object.position.set(0, 0, 0); + object.name = 'Cube2ndScene'; + scene2.name = 'Scene2'; + scene2.add(object); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1; + + container.appendChild(renderer.domElement); + + // + + window.addEventListener('resize', onWindowResize); + + // --------------------------------------------------------------------- + // Exporting compressed textures and meshes (KTX2 / Draco / Meshopt) + // --------------------------------------------------------------------- + const ktx2Loader = new KTX2Loader().setTranscoderPath('jsm/libs/basis/').detectSupport(renderer); + + const gltfLoader = new GLTFLoader().setPath('models/gltf/'); + gltfLoader.setKTX2Loader(ktx2Loader); + gltfLoader.setMeshoptDecoder(MeshoptDecoder); + gltfLoader.load('coffeemat.glb', function (gltf) { + gltf.scene.position.x = 400; + gltf.scene.position.z = -200; + + scene1.add(gltf.scene); + + coffeemat = gltf.scene; + }); + + // + + const gui = new GUI(); + + let h = gui.addFolder('Settings'); + h.add(params, 'trs').name('Use TRS'); + h.add(params, 'onlyVisible').name('Only Visible Objects'); + h.add(params, 'binary').name('Binary (GLB)'); + h.add(params, 'maxTextureSize', 2, 8192).name('Max Texture Size').step(1); + + h = gui.addFolder('Export'); + h.add(params, 'exportScene1').name('Export Scene 1'); + h.add(params, 'exportScenes').name('Export Scene 1 and 2'); + h.add(params, 'exportSphere').name('Export Sphere'); + h.add(params, 'exportModel').name('Export Model'); + h.add(params, 'exportObjects').name('Export Sphere With Grid'); + h.add(params, 'exportSceneObject').name('Export Scene 1 and Object'); + h.add(params, 'exportCompressedObject').name('Export Coffeemat (from compressed data)'); + + gui.open(); +} + +function exportScene1() { + exportGLTF(scene1); +} + +function exportScenes() { + exportGLTF([scene1, scene2]); +} + +function exportSphere() { + exportGLTF(sphere); +} + +function exportModel() { + exportGLTF(model); +} + +function exportObjects() { + exportGLTF([sphere, gridHelper]); +} + +function exportSceneObject() { + exportGLTF([scene1, gridHelper]); +} + +function exportCompressedObject() { + exportGLTF([coffeemat]); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const timer = Date.now() * 0.0001; + + camera.position.x = Math.cos(timer) * 800; + camera.position.z = Math.sin(timer) * 800; + + camera.lookAt(scene1.position); + renderer.render(scene1, camera); +} diff --git a/examples-testing/examples/misc_exporter_obj.ts b/examples-testing/examples/misc_exporter_obj.ts new file mode 100644 index 000000000..025034daf --- /dev/null +++ b/examples-testing/examples/misc_exporter_obj.ts @@ -0,0 +1,192 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { OBJExporter } from 'three/addons/exporters/OBJExporter.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer; + +const params = { + addTriangle: addTriangle, + addCube: addCube, + addCylinder: addCylinder, + addMultiple: addMultiple, + addTransformed: addTransformed, + addPoints: addPoints, + exportToObj: exportToObj, +}; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 0, 400); + + scene = new THREE.Scene(); + + const ambientLight = new THREE.AmbientLight(0xffffff); + scene.add(ambientLight); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 2.5); + directionalLight.position.set(0, 1, 1); + scene.add(directionalLight); + + const gui = new GUI(); + + let h = gui.addFolder('Geometry Selection'); + h.add(params, 'addTriangle').name('Triangle'); + h.add(params, 'addCube').name('Cube'); + h.add(params, 'addCylinder').name('Cylinder'); + h.add(params, 'addMultiple').name('Multiple objects'); + h.add(params, 'addTransformed').name('Transformed objects'); + h.add(params, 'addPoints').name('Point Cloud'); + + h = gui.addFolder('Export'); + h.add(params, 'exportToObj').name('Export OBJ'); + + gui.open(); + + addGeometry(1); + + window.addEventListener('resize', onWindowResize); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.enablePan = false; +} + +function exportToObj() { + const exporter = new OBJExporter(); + const result = exporter.parse(scene); + saveString(result, 'object.obj'); +} + +function addGeometry(type) { + for (let i = 0; i < scene.children.length; i++) { + const child = scene.children[i]; + + if (child.isMesh || child.isPoints) { + child.geometry.dispose(); + scene.remove(child); + i--; + } + } + + if (type === 1) { + const material = new THREE.MeshLambertMaterial({ color: 0x00cc00 }); + const geometry = generateTriangleGeometry(); + + scene.add(new THREE.Mesh(geometry, material)); + } else if (type === 2) { + const material = new THREE.MeshLambertMaterial({ color: 0x00cc00 }); + const geometry = new THREE.BoxGeometry(100, 100, 100); + scene.add(new THREE.Mesh(geometry, material)); + } else if (type === 3) { + const material = new THREE.MeshLambertMaterial({ color: 0x00cc00 }); + const geometry = new THREE.CylinderGeometry(50, 50, 100, 30, 1); + scene.add(new THREE.Mesh(geometry, material)); + } else if (type === 4 || type === 5) { + const material = new THREE.MeshLambertMaterial({ color: 0x00cc00 }); + const geometry = generateTriangleGeometry(); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.x = -200; + scene.add(mesh); + + const geometry2 = new THREE.BoxGeometry(100, 100, 100); + const mesh2 = new THREE.Mesh(geometry2, material); + scene.add(mesh2); + + const geometry3 = new THREE.CylinderGeometry(50, 50, 100, 30, 1); + const mesh3 = new THREE.Mesh(geometry3, material); + mesh3.position.x = 200; + scene.add(mesh3); + + if (type === 5) { + mesh.rotation.y = Math.PI / 4.0; + mesh2.rotation.y = Math.PI / 4.0; + mesh3.rotation.y = Math.PI / 4.0; + } + } else if (type === 6) { + const points = [0, 0, 0, 100, 0, 0, 100, 100, 0, 0, 100, 0]; + const colors = [0.5, 0, 0, 0.5, 0, 0, 0, 0.5, 0, 0, 0.5, 0]; + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.Float32BufferAttribute(points, 3)); + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + + const material = new THREE.PointsMaterial({ size: 10, vertexColors: true }); + + const pointCloud = new THREE.Points(geometry, material); + pointCloud.name = 'point cloud'; + scene.add(pointCloud); + } +} + +function addTriangle() { + addGeometry(1); +} + +function addCube() { + addGeometry(2); +} + +function addCylinder() { + addGeometry(3); +} + +function addMultiple() { + addGeometry(4); +} + +function addTransformed() { + addGeometry(5); +} + +function addPoints() { + addGeometry(6); +} + +const link = document.createElement('a'); +link.style.display = 'none'; +document.body.appendChild(link); + +function save(blob, filename) { + link.href = URL.createObjectURL(blob); + link.download = filename; + link.click(); +} + +function saveString(text, filename) { + save(new Blob([text], { type: 'text/plain' }), filename); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} + +function generateTriangleGeometry() { + const geometry = new THREE.BufferGeometry(); + const vertices = []; + + vertices.push(-50, -50, 0); + vertices.push(50, -50, 0); + vertices.push(50, 50, 0); + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + geometry.computeVertexNormals(); + + return geometry; +} diff --git a/examples-testing/examples/misc_exporter_ply.ts b/examples-testing/examples/misc_exporter_ply.ts new file mode 100644 index 000000000..b7e324688 --- /dev/null +++ b/examples-testing/examples/misc_exporter_ply.ts @@ -0,0 +1,156 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { PLYExporter } from 'three/addons/exporters/PLYExporter.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let scene, camera, renderer, exporter, mesh; + +const params = { + exportASCII: exportASCII, + exportBinaryBigEndian: exportBinaryBigEndian, + exportBinaryLittleEndian: exportBinaryLittleEndian, +}; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(4, 2, 4); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xa0a0a0); + scene.fog = new THREE.Fog(0xa0a0a0, 4, 20); + + exporter = new PLYExporter(); + + // + + const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 3); + hemiLight.position.set(0, 20, 0); + scene.add(hemiLight); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 3); + directionalLight.position.set(0, 20, 10); + directionalLight.castShadow = true; + directionalLight.shadow.camera.top = 2; + directionalLight.shadow.camera.bottom = -2; + directionalLight.shadow.camera.left = -2; + directionalLight.shadow.camera.right = 2; + scene.add(directionalLight); + + // ground + + const ground = new THREE.Mesh( + new THREE.PlaneGeometry(40, 40), + new THREE.MeshPhongMaterial({ color: 0xcbcbcb, depthWrite: false }), + ); + ground.rotation.x = -Math.PI / 2; + ground.receiveShadow = true; + scene.add(ground); + + const grid = new THREE.GridHelper(40, 20, 0x000000, 0x000000); + grid.material.opacity = 0.2; + grid.material.transparent = true; + scene.add(grid); + + // export mesh + + const geometry = new THREE.BoxGeometry(); + const material = new THREE.MeshPhongMaterial({ vertexColors: true }); + + // color vertices based on vertex positions + const colors = geometry.getAttribute('position').array.slice(); + for (let i = 0, l = colors.length; i < l; i++) { + if (colors[i] > 0) colors[i] = 0.5; + else colors[i] = 0; + } + + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3, false)); + + mesh = new THREE.Mesh(geometry, material); + mesh.castShadow = true; + mesh.position.y = 0.5; + scene.add(mesh); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + document.body.appendChild(renderer.domElement); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 0.5, 0); + controls.update(); + + // + + window.addEventListener('resize', onWindowResize); + + const gui = new GUI(); + + gui.add(params, 'exportASCII').name('Export PLY (ASCII)'); + gui.add(params, 'exportBinaryBigEndian').name('Export PLY (Binary BE)'); + gui.add(params, 'exportBinaryLittleEndian').name('Export PLY (Binary LE)'); + gui.open(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} + +function exportASCII() { + exporter.parse(mesh, function (result) { + saveString(result, 'box.ply'); + }); +} + +function exportBinaryBigEndian() { + exporter.parse( + mesh, + function (result) { + saveArrayBuffer(result, 'box.ply'); + }, + { binary: true }, + ); +} + +function exportBinaryLittleEndian() { + exporter.parse( + mesh, + function (result) { + saveArrayBuffer(result, 'box.ply'); + }, + { binary: true, littleEndian: true }, + ); +} + +const link = document.createElement('a'); +link.style.display = 'none'; +document.body.appendChild(link); + +function save(blob, filename) { + link.href = URL.createObjectURL(blob); + link.download = filename; + link.click(); +} + +function saveString(text, filename) { + save(new Blob([text], { type: 'text/plain' }), filename); +} + +function saveArrayBuffer(buffer, filename) { + save(new Blob([buffer], { type: 'application/octet-stream' }), filename); +} diff --git a/examples-testing/examples/misc_exporter_stl.ts b/examples-testing/examples/misc_exporter_stl.ts new file mode 100644 index 000000000..ff6d6e2b5 --- /dev/null +++ b/examples-testing/examples/misc_exporter_stl.ts @@ -0,0 +1,129 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { STLExporter } from 'three/addons/exporters/STLExporter.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let scene, camera, renderer, exporter, mesh; + +const params = { + exportASCII: exportASCII, + exportBinary: exportBinary, +}; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(4, 2, 4); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xa0a0a0); + scene.fog = new THREE.Fog(0xa0a0a0, 4, 20); + + exporter = new STLExporter(); + + // + + const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 3); + hemiLight.position.set(0, 20, 0); + scene.add(hemiLight); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 3); + directionalLight.position.set(0, 20, 10); + directionalLight.castShadow = true; + directionalLight.shadow.camera.top = 2; + directionalLight.shadow.camera.bottom = -2; + directionalLight.shadow.camera.left = -2; + directionalLight.shadow.camera.right = 2; + scene.add(directionalLight); + + // ground + + const ground = new THREE.Mesh( + new THREE.PlaneGeometry(40, 40), + new THREE.MeshPhongMaterial({ color: 0xbbbbbb, depthWrite: false }), + ); + ground.rotation.x = -Math.PI / 2; + ground.receiveShadow = true; + scene.add(ground); + + const grid = new THREE.GridHelper(40, 20, 0x000000, 0x000000); + grid.material.opacity = 0.2; + grid.material.transparent = true; + scene.add(grid); + + // export mesh + + const geometry = new THREE.BoxGeometry(); + const material = new THREE.MeshPhongMaterial({ color: 0x00ff00 }); + + mesh = new THREE.Mesh(geometry, material); + mesh.castShadow = true; + mesh.position.y = 0.5; + scene.add(mesh); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + document.body.appendChild(renderer.domElement); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 0.5, 0); + controls.update(); + + // + + window.addEventListener('resize', onWindowResize); + + const gui = new GUI(); + + gui.add(params, 'exportASCII').name('Export STL (ASCII)'); + gui.add(params, 'exportBinary').name('Export STL (Binary)'); + gui.open(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} + +function exportASCII() { + const result = exporter.parse(mesh); + saveString(result, 'box.stl'); +} + +function exportBinary() { + const result = exporter.parse(mesh, { binary: true }); + saveArrayBuffer(result, 'box.stl'); +} + +const link = document.createElement('a'); +link.style.display = 'none'; +document.body.appendChild(link); + +function save(blob, filename) { + link.href = URL.createObjectURL(blob); + link.download = filename; + link.click(); +} + +function saveString(text, filename) { + save(new Blob([text], { type: 'text/plain' }), filename); +} + +function saveArrayBuffer(buffer, filename) { + save(new Blob([buffer], { type: 'application/octet-stream' }), filename); +} diff --git a/examples-testing/examples/misc_exporter_usdz.ts b/examples-testing/examples/misc_exporter_usdz.ts new file mode 100644 index 000000000..9a14919ba --- /dev/null +++ b/examples-testing/examples/misc_exporter_usdz.ts @@ -0,0 +1,129 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { USDZExporter } from 'three/addons/exporters/USDZExporter.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer; + +const params = { + exportUSDZ: exportUSDZ, +}; + +init(); +render(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + document.body.appendChild(renderer.domElement); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 20); + camera.position.set(-2.5, 0.6, 3.0); + + const pmremGenerator = new THREE.PMREMGenerator(renderer); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture; + + const loader = new GLTFLoader().setPath('models/gltf/DamagedHelmet/glTF/'); + loader.load('DamagedHelmet.gltf', async function (gltf) { + scene.add(gltf.scene); + + const shadowMesh = createSpotShadowMesh(); + shadowMesh.position.y = -1.1; + shadowMesh.position.z = -0.25; + shadowMesh.scale.setScalar(2); + scene.add(shadowMesh); + + render(); + + // USDZ + + const exporter = new USDZExporter(); + const arraybuffer = await exporter.parseAsync(gltf.scene); + const blob = new Blob([arraybuffer], { type: 'application/octet-stream' }); + + const link = document.getElementById('link'); + link.href = URL.createObjectURL(blob); + }); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use if there is no animation loop + controls.minDistance = 2; + controls.maxDistance = 10; + controls.target.set(0, -0.15, -0.2); + controls.update(); + + window.addEventListener('resize', onWindowResize); + + const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); + + if (isIOS === false) { + const gui = new GUI(); + + gui.add(params, 'exportUSDZ').name('Export USDZ'); + gui.open(); + } +} + +function createSpotShadowMesh() { + const canvas = document.createElement('canvas'); + canvas.width = 128; + canvas.height = 128; + + const context = canvas.getContext('2d'); + const gradient = context.createRadialGradient( + canvas.width / 2, + canvas.height / 2, + 0, + canvas.width / 2, + canvas.height / 2, + canvas.width / 2, + ); + gradient.addColorStop(0.1, 'rgba(130,130,130,1)'); + gradient.addColorStop(1, 'rgba(255,255,255,1)'); + + context.fillStyle = gradient; + context.fillRect(0, 0, canvas.width, canvas.height); + + const shadowTexture = new THREE.CanvasTexture(canvas); + + const geometry = new THREE.PlaneGeometry(); + const material = new THREE.MeshBasicMaterial({ + map: shadowTexture, + blending: THREE.MultiplyBlending, + toneMapped: false, + }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.rotation.x = -Math.PI / 2; + + return mesh; +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function exportUSDZ() { + const link = document.getElementById('link'); + link.click(); +} + +// + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/misc_lookat.ts b/examples-testing/examples/misc_lookat.ts new file mode 100644 index 000000000..280b6e2d8 --- /dev/null +++ b/examples-testing/examples/misc_lookat.ts @@ -0,0 +1,95 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let camera, scene, renderer, stats; + +let sphere; + +let mouseX = 0, + mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +document.addEventListener('mousemove', onDocumentMouseMove); + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 15000); + camera.position.z = 3200; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + + sphere = new THREE.Mesh(new THREE.SphereGeometry(100, 20, 20), new THREE.MeshNormalMaterial()); + scene.add(sphere); + + const geometry = new THREE.CylinderGeometry(0, 10, 100, 12); + geometry.rotateX(Math.PI / 2); + + const material = new THREE.MeshNormalMaterial(); + + for (let i = 0; i < 1000; i++) { + const mesh = new THREE.Mesh(geometry, material); + mesh.position.x = Math.random() * 4000 - 2000; + mesh.position.y = Math.random() * 4000 - 2000; + mesh.position.z = Math.random() * 4000 - 2000; + mesh.scale.x = mesh.scale.y = mesh.scale.z = Math.random() * 4 + 2; + scene.add(mesh); + } + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onDocumentMouseMove(event) { + mouseX = (event.clientX - windowHalfX) * 10; + mouseY = (event.clientY - windowHalfY) * 10; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + const time = Date.now() * 0.0005; + + sphere.position.x = Math.sin(time * 0.7) * 2000; + sphere.position.y = Math.cos(time * 0.5) * 2000; + sphere.position.z = Math.cos(time * 0.3) * 2000; + + for (let i = 1, l = scene.children.length; i < l; i++) { + scene.children[i].lookAt(sphere.position); + } + + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-mouseY - camera.position.y) * 0.05; + camera.lookAt(scene.position); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/misc_uv_tests.ts b/examples-testing/examples/misc_uv_tests.ts new file mode 100644 index 000000000..4f782d45f --- /dev/null +++ b/examples-testing/examples/misc_uv_tests.ts @@ -0,0 +1,44 @@ +import * as THREE from 'three'; + +import { UVsDebug } from 'three/addons/utils/UVsDebug.js'; + +/* + * This is to help debug UVs problems in geometry, + * as well as allow a new user to visualize what UVs are about. + */ + +function test(name, geometry) { + const d = document.createElement('div'); + + d.innerHTML = '

' + name + '

'; + + d.appendChild(UVsDebug(geometry)); + + document.body.appendChild(d); +} + +const points = []; + +for (let i = 0; i < 10; i++) { + points.push(new THREE.Vector2(Math.sin(i * 0.2) * 15 + 50, (i - 5) * 2)); +} + +// + +test('new THREE.PlaneGeometry( 100, 100, 4, 4 )', new THREE.PlaneGeometry(100, 100, 4, 4)); + +test('new THREE.SphereGeometry( 75, 12, 6 )', new THREE.SphereGeometry(75, 12, 6)); + +test('new THREE.IcosahedronGeometry( 30, 1 )', new THREE.IcosahedronGeometry(30, 1)); + +test('new THREE.OctahedronGeometry( 30, 2 )', new THREE.OctahedronGeometry(30, 2)); + +test('new THREE.CylinderGeometry( 25, 75, 100, 10, 5 )', new THREE.CylinderGeometry(25, 75, 100, 10, 5)); + +test('new THREE.BoxGeometry( 100, 100, 100, 4, 4, 4 )', new THREE.BoxGeometry(100, 100, 100, 4, 4, 4)); + +test('new THREE.LatheGeometry( points, 8 )', new THREE.LatheGeometry(points, 8)); + +test('new THREE.TorusGeometry( 50, 20, 8, 8 )', new THREE.TorusGeometry(50, 20, 8, 8)); + +test('new THREE.TorusKnotGeometry( 50, 10, 12, 6 )', new THREE.TorusKnotGeometry(50, 10, 12, 6)); diff --git a/examples-testing/examples/physics_ammo_instancing.ts b/examples-testing/examples/physics_ammo_instancing.ts new file mode 100644 index 000000000..265c254c8 --- /dev/null +++ b/examples-testing/examples/physics_ammo_instancing.ts @@ -0,0 +1,119 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { AmmoPhysics } from 'three/addons/physics/AmmoPhysics.js'; +import Stats from 'three/addons/libs/stats.module.js'; + +let camera, scene, renderer, stats; +let physics, position; + +let boxes, spheres; + +init(); + +async function init() { + physics = await AmmoPhysics(); + position = new THREE.Vector3(); + + // + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(-1, 1.5, 2); + camera.lookAt(0, 0.5, 0); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x666666); + + const hemiLight = new THREE.HemisphereLight(); + scene.add(hemiLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.position.set(5, 5, 5); + dirLight.castShadow = true; + dirLight.shadow.camera.zoom = 2; + scene.add(dirLight); + + const floor = new THREE.Mesh(new THREE.BoxGeometry(10, 5, 10), new THREE.ShadowMaterial({ color: 0x444444 })); + floor.position.y = -2.5; + floor.receiveShadow = true; + floor.userData.physics = { mass: 0 }; + scene.add(floor); + + // + + const material = new THREE.MeshLambertMaterial(); + + const matrix = new THREE.Matrix4(); + const color = new THREE.Color(); + + // Boxes + + const geometryBox = new THREE.BoxGeometry(0.075, 0.075, 0.075); + boxes = new THREE.InstancedMesh(geometryBox, material, 400); + boxes.instanceMatrix.setUsage(THREE.DynamicDrawUsage); // will be updated every frame + boxes.castShadow = true; + boxes.receiveShadow = true; + boxes.userData.physics = { mass: 1 }; + scene.add(boxes); + + for (let i = 0; i < boxes.count; i++) { + matrix.setPosition(Math.random() - 0.5, Math.random() * 2, Math.random() - 0.5); + boxes.setMatrixAt(i, matrix); + boxes.setColorAt(i, color.setHex(0xffffff * Math.random())); + } + + // Spheres + + const geometrySphere = new THREE.IcosahedronGeometry(0.05, 4); + spheres = new THREE.InstancedMesh(geometrySphere, material, 400); + spheres.instanceMatrix.setUsage(THREE.DynamicDrawUsage); // will be updated every frame + spheres.castShadow = true; + spheres.receiveShadow = true; + spheres.userData.physics = { mass: 1 }; + scene.add(spheres); + + for (let i = 0; i < spheres.count; i++) { + matrix.setPosition(Math.random() - 0.5, Math.random() * 2, Math.random() - 0.5); + spheres.setMatrixAt(i, matrix); + spheres.setColorAt(i, color.setHex(0xffffff * Math.random())); + } + + physics.addScene(scene); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.y = 0.5; + controls.update(); + + setInterval(() => { + let index = Math.floor(Math.random() * boxes.count); + + position.set(0, Math.random() + 1, 0); + physics.setMeshPosition(boxes, position, index); + + // + + index = Math.floor(Math.random() * spheres.count); + + position.set(0, Math.random() + 1, 0); + physics.setMeshPosition(spheres, position, index); + }, 1000 / 60); +} + +function animate() { + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/physics_jolt_instancing.ts b/examples-testing/examples/physics_jolt_instancing.ts new file mode 100644 index 000000000..022263c0d --- /dev/null +++ b/examples-testing/examples/physics_jolt_instancing.ts @@ -0,0 +1,119 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { JoltPhysics } from 'three/addons/physics/JoltPhysics.js'; +import Stats from 'three/addons/libs/stats.module.js'; + +let camera, scene, renderer, stats; +let physics, position; + +let boxes, spheres; + +init(); + +async function init() { + physics = await JoltPhysics(); + position = new THREE.Vector3(); + + // + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(-1, 1.5, 2); + camera.lookAt(0, 0.5, 0); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x666666); + + const hemiLight = new THREE.HemisphereLight(); + scene.add(hemiLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.position.set(5, 5, 5); + dirLight.castShadow = true; + dirLight.shadow.camera.zoom = 2; + scene.add(dirLight); + + const floor = new THREE.Mesh(new THREE.BoxGeometry(10, 5, 10), new THREE.ShadowMaterial({ color: 0x444444 })); + floor.position.y = -2.5; + floor.receiveShadow = true; + floor.userData.physics = { mass: 0 }; + scene.add(floor); + + // + + const material = new THREE.MeshLambertMaterial(); + + const matrix = new THREE.Matrix4(); + const color = new THREE.Color(); + + // Boxes + + const geometryBox = new THREE.BoxGeometry(0.075, 0.075, 0.075); + boxes = new THREE.InstancedMesh(geometryBox, material, 400); + boxes.instanceMatrix.setUsage(THREE.DynamicDrawUsage); // will be updated every frame + boxes.castShadow = true; + boxes.receiveShadow = true; + boxes.userData.physics = { mass: 1 }; + scene.add(boxes); + + for (let i = 0; i < boxes.count; i++) { + matrix.setPosition(Math.random() - 0.5, Math.random() * 2, Math.random() - 0.5); + boxes.setMatrixAt(i, matrix); + boxes.setColorAt(i, color.setHex(0xffffff * Math.random())); + } + + // Spheres + + const geometrySphere = new THREE.IcosahedronGeometry(0.05, 4); + spheres = new THREE.InstancedMesh(geometrySphere, material, 400); + spheres.instanceMatrix.setUsage(THREE.DynamicDrawUsage); // will be updated every frame + spheres.castShadow = true; + spheres.receiveShadow = true; + spheres.userData.physics = { mass: 1 }; + scene.add(spheres); + + for (let i = 0; i < spheres.count; i++) { + matrix.setPosition(Math.random() - 0.5, Math.random() * 2, Math.random() - 0.5); + spheres.setMatrixAt(i, matrix); + spheres.setColorAt(i, color.setHex(0xffffff * Math.random())); + } + + physics.addScene(scene); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.y = 0.5; + controls.update(); + + setInterval(() => { + let index = Math.floor(Math.random() * boxes.count); + + position.set(0, Math.random() + 1, 0); + physics.setMeshPosition(boxes, position, index); + + // + + index = Math.floor(Math.random() * spheres.count); + + position.set(0, Math.random() + 1, 0); + physics.setMeshPosition(spheres, position, index); + }, 1000 / 60); +} + +function animate() { + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/physics_rapier_instancing.ts b/examples-testing/examples/physics_rapier_instancing.ts new file mode 100644 index 000000000..f23cf7667 --- /dev/null +++ b/examples-testing/examples/physics_rapier_instancing.ts @@ -0,0 +1,119 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { RapierPhysics } from 'three/addons/physics/RapierPhysics.js'; +import Stats from 'three/addons/libs/stats.module.js'; + +let camera, scene, renderer, stats; +let physics, position; + +let boxes, spheres; + +init(); + +async function init() { + physics = await RapierPhysics(); + position = new THREE.Vector3(); + + // + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(-1, 1.5, 2); + camera.lookAt(0, 0.5, 0); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x666666); + + const hemiLight = new THREE.HemisphereLight(); + scene.add(hemiLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.position.set(5, 5, 5); + dirLight.castShadow = true; + dirLight.shadow.camera.zoom = 2; + scene.add(dirLight); + + const floor = new THREE.Mesh(new THREE.BoxGeometry(10, 5, 10), new THREE.ShadowMaterial({ color: 0x444444 })); + floor.position.y = -2.5; + floor.receiveShadow = true; + floor.userData.physics = { mass: 0 }; + scene.add(floor); + + // + + const material = new THREE.MeshLambertMaterial(); + + const matrix = new THREE.Matrix4(); + const color = new THREE.Color(); + + // Boxes + + const geometryBox = new THREE.BoxGeometry(0.075, 0.075, 0.075); + boxes = new THREE.InstancedMesh(geometryBox, material, 400); + boxes.instanceMatrix.setUsage(THREE.DynamicDrawUsage); // will be updated every frame + boxes.castShadow = true; + boxes.receiveShadow = true; + boxes.userData.physics = { mass: 1 }; + scene.add(boxes); + + for (let i = 0; i < boxes.count; i++) { + matrix.setPosition(Math.random() - 0.5, Math.random() * 2, Math.random() - 0.5); + boxes.setMatrixAt(i, matrix); + boxes.setColorAt(i, color.setHex(0xffffff * Math.random())); + } + + // Spheres + + const geometrySphere = new THREE.IcosahedronGeometry(0.05, 4); + spheres = new THREE.InstancedMesh(geometrySphere, material, 400); + spheres.instanceMatrix.setUsage(THREE.DynamicDrawUsage); // will be updated every frame + spheres.castShadow = true; + spheres.receiveShadow = true; + spheres.userData.physics = { mass: 1 }; + scene.add(spheres); + + for (let i = 0; i < spheres.count; i++) { + matrix.setPosition(Math.random() - 0.5, Math.random() * 2, Math.random() - 0.5); + spheres.setMatrixAt(i, matrix); + spheres.setColorAt(i, color.setHex(0xffffff * Math.random())); + } + + physics.addScene(scene); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.y = 0.5; + controls.update(); + + setInterval(() => { + let index = Math.floor(Math.random() * boxes.count); + + position.set(0, Math.random() + 1, 0); + physics.setMeshPosition(boxes, position, index); + + // + + index = Math.floor(Math.random() * spheres.count); + + position.set(0, Math.random() + 1, 0); + physics.setMeshPosition(spheres, position, index); + }, 1000 / 60); +} + +function animate() { + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/svg_lines.ts b/examples-testing/examples/svg_lines.ts new file mode 100644 index 000000000..99b74c405 --- /dev/null +++ b/examples-testing/examples/svg_lines.ts @@ -0,0 +1,87 @@ +import * as THREE from 'three'; + +import { SVGRenderer } from 'three/addons/renderers/SVGRenderer.js'; + +THREE.ColorManagement.enabled = false; + +let camera, scene, renderer; + +init(); +animate(); + +function init() { + camera = new THREE.PerspectiveCamera(33, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.z = 10; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0, 0, 0); + + renderer = new SVGRenderer(); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + // + + const vertices = []; + const divisions = 50; + + for (let i = 0; i <= divisions; i++) { + const v = (i / divisions) * (Math.PI * 2); + + const x = Math.sin(v); + const z = Math.cos(v); + + vertices.push(x, 0, z); + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + + // + + for (let i = 1; i <= 3; i++) { + const material = new THREE.LineBasicMaterial({ + color: Math.random() * 0xffffff, + linewidth: 10, + }); + const line = new THREE.Line(geometry, material); + line.scale.setScalar(i / 3); + scene.add(line); + } + + const material = new THREE.LineDashedMaterial({ + color: 'blue', + linewidth: 1, + dashSize: 10, + gapSize: 10, + }); + const line = new THREE.Line(geometry, material); + line.scale.setScalar(2); + scene.add(line); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + let count = 0; + const time = performance.now() / 1000; + + scene.traverse(function (child) { + child.rotation.x = count + time / 3; + child.rotation.z = count + time / 4; + + count++; + }); + + renderer.render(scene, camera); + requestAnimationFrame(animate); +} diff --git a/examples-testing/examples/svg_sandbox.ts b/examples-testing/examples/svg_sandbox.ts new file mode 100644 index 000000000..e6be8386c --- /dev/null +++ b/examples-testing/examples/svg_sandbox.ts @@ -0,0 +1,212 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { SVGRenderer, SVGObject } from 'three/addons/renderers/SVGRenderer.js'; + +THREE.ColorManagement.enabled = false; + +let camera, scene, renderer, stats; + +let group; + +init(); +animate(); + +function init() { + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 500; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + // QRCODE + + const loader = new THREE.BufferGeometryLoader(); + loader.load('models/json/QRCode_buffergeometry.json', function (geometry) { + mesh = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ vertexColors: true })); + mesh.scale.x = mesh.scale.y = mesh.scale.z = 2; + scene.add(mesh); + }); + + // CUBES + + const boxGeometry = new THREE.BoxGeometry(100, 100, 100); + + let mesh = new THREE.Mesh( + boxGeometry, + new THREE.MeshBasicMaterial({ color: 0x0000ff, opacity: 0.5, transparent: true }), + ); + mesh.position.x = 500; + mesh.rotation.x = Math.random(); + mesh.rotation.y = Math.random(); + mesh.scale.x = mesh.scale.y = mesh.scale.z = 2; + scene.add(mesh); + + mesh = new THREE.Mesh(boxGeometry, new THREE.MeshBasicMaterial({ color: Math.random() * 0xffffff })); + mesh.position.x = 500; + mesh.position.y = 500; + mesh.rotation.x = Math.random(); + mesh.rotation.y = Math.random(); + mesh.scale.x = mesh.scale.y = mesh.scale.z = 2; + scene.add(mesh); + + // PLANE + + mesh = new THREE.Mesh( + new THREE.PlaneGeometry(100, 100), + new THREE.MeshBasicMaterial({ color: Math.random() * 0xffffff, side: THREE.DoubleSide }), + ); + mesh.position.y = -500; + mesh.scale.x = mesh.scale.y = mesh.scale.z = 2; + scene.add(mesh); + + // CYLINDER + + mesh = new THREE.Mesh( + new THREE.CylinderGeometry(20, 100, 200, 10), + new THREE.MeshBasicMaterial({ color: Math.random() * 0xffffff }), + ); + mesh.position.x = -500; + mesh.rotation.x = -Math.PI / 2; + mesh.scale.x = mesh.scale.y = mesh.scale.z = 2; + scene.add(mesh); + + // POLYFIELD + + const geometry = new THREE.BufferGeometry(); + const material = new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.DoubleSide }); + + const v = new THREE.Vector3(); + const v0 = new THREE.Vector3(); + const v1 = new THREE.Vector3(); + const v2 = new THREE.Vector3(); + const color = new THREE.Color(); + + const vertices = []; + const colors = []; + + for (let i = 0; i < 100; i++) { + v.set(Math.random() * 1000 - 500, Math.random() * 1000 - 500, Math.random() * 1000 - 500); + + v0.set(Math.random() * 100 - 50, Math.random() * 100 - 50, Math.random() * 100 - 50); + + v1.set(Math.random() * 100 - 50, Math.random() * 100 - 50, Math.random() * 100 - 50); + + v2.set(Math.random() * 100 - 50, Math.random() * 100 - 50, Math.random() * 100 - 50); + + v0.add(v); + v1.add(v); + v2.add(v); + + color.setHex(Math.random() * 0xffffff); + + // create a single triangle + + vertices.push(v0.x, v0.y, v0.z); + vertices.push(v1.x, v1.y, v1.z); + vertices.push(v2.x, v2.y, v2.z); + + colors.push(color.r, color.g, color.b); + colors.push(color.r, color.g, color.b); + colors.push(color.r, color.g, color.b); + } + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + + group = new THREE.Mesh(geometry, material); + group.scale.set(2, 2, 2); + scene.add(group); + + // SPRITES + + for (let i = 0; i < 50; i++) { + const material = new THREE.SpriteMaterial({ color: Math.random() * 0xffffff }); + const sprite = new THREE.Sprite(material); + sprite.position.x = Math.random() * 1000 - 500; + sprite.position.y = Math.random() * 1000 - 500; + sprite.position.z = Math.random() * 1000 - 500; + sprite.scale.set(64, 64, 1); + scene.add(sprite); + } + + // CUSTOM + + const node = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + node.setAttribute('stroke', 'black'); + node.setAttribute('fill', 'red'); + node.setAttribute('r', '40'); + + for (let i = 0; i < 50; i++) { + const object = new SVGObject(node.cloneNode()); + object.position.x = Math.random() * 1000 - 500; + object.position.y = Math.random() * 1000 - 500; + object.position.z = Math.random() * 1000 - 500; + scene.add(object); + } + + // CUSTOM FROM FILE + + const fileLoader = new THREE.FileLoader(); + fileLoader.load('models/svg/hexagon.svg', function (svg) { + const node = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + const parser = new DOMParser(); + const doc = parser.parseFromString(svg, 'image/svg+xml'); + + node.appendChild(doc.documentElement); + + const object = new SVGObject(node); + object.position.x = 500; + scene.add(object); + }); + + // LIGHTS + + const ambient = new THREE.AmbientLight(0x80ffff); + scene.add(ambient); + + const directional = new THREE.DirectionalLight(0xffff00); + directional.position.set(-1, 0.5, 0); + scene.add(directional); + + renderer = new SVGRenderer(); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setQuality('low'); + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + requestAnimationFrame(animate); + + render(); + stats.update(); +} + +function render() { + const time = Date.now() * 0.0002; + + camera.position.x = Math.sin(time) * 500; + camera.position.z = Math.cos(time) * 500; + camera.lookAt(scene.position); + + group.rotation.x += 0.01; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webaudio_orientation.ts b/examples-testing/examples/webaudio_orientation.ts new file mode 100644 index 000000000..7baaa88a0 --- /dev/null +++ b/examples-testing/examples/webaudio_orientation.ts @@ -0,0 +1,141 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { PositionalAudioHelper } from 'three/addons/helpers/PositionalAudioHelper.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +let scene, camera, renderer; + +const startButton = document.getElementById('startButton'); +startButton.addEventListener('click', init); + +function init() { + const overlay = document.getElementById('overlay'); + overlay.remove(); + + const container = document.getElementById('container'); + + // + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(3, 2, 3); + + const reflectionCube = new THREE.CubeTextureLoader() + .setPath('textures/cube/SwedishRoyalCastle/') + .load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xa0a0a0); + scene.fog = new THREE.Fog(0xa0a0a0, 2, 20); + + // + + const hemiLight = new THREE.HemisphereLight(0xffffff, 0x8d8d8d, 3); + hemiLight.position.set(0, 20, 0); + scene.add(hemiLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.position.set(5, 5, 0); + dirLight.castShadow = true; + dirLight.shadow.camera.top = 1; + dirLight.shadow.camera.bottom = -1; + dirLight.shadow.camera.left = -1; + dirLight.shadow.camera.right = 1; + dirLight.shadow.camera.near = 0.1; + dirLight.shadow.camera.far = 20; + scene.add(dirLight); + + // scene.add( new THREE.CameraHelper( dirLight.shadow.camera ) ); + + // + + const mesh = new THREE.Mesh( + new THREE.PlaneGeometry(50, 50), + new THREE.MeshPhongMaterial({ color: 0xcbcbcb, depthWrite: false }), + ); + mesh.rotation.x = -Math.PI / 2; + mesh.receiveShadow = true; + scene.add(mesh); + + const grid = new THREE.GridHelper(50, 50, 0xc1c1c1, 0xc1c1c1); + scene.add(grid); + + // + + const listener = new THREE.AudioListener(); + camera.add(listener); + + const audioElement = document.getElementById('music'); + audioElement.play(); + + const positionalAudio = new THREE.PositionalAudio(listener); + positionalAudio.setMediaElementSource(audioElement); + positionalAudio.setRefDistance(1); + positionalAudio.setDirectionalCone(180, 230, 0.1); + + const helper = new PositionalAudioHelper(positionalAudio, 0.1); + positionalAudio.add(helper); + + // + + const gltfLoader = new GLTFLoader(); + gltfLoader.load('models/gltf/BoomBox.glb', function (gltf) { + const boomBox = gltf.scene; + boomBox.position.set(0, 0.2, 0); + boomBox.scale.set(20, 20, 20); + + boomBox.traverse(function (object) { + if (object.isMesh) { + object.material.envMap = reflectionCube; + object.geometry.rotateY(-Math.PI); + object.castShadow = true; + } + }); + + boomBox.add(positionalAudio); + scene.add(boomBox); + + renderer.setAnimationLoop(animate); + }); + + // sound is damped behind this wall + + const wallGeometry = new THREE.BoxGeometry(2, 1, 0.1); + const wallMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.5 }); + + const wall = new THREE.Mesh(wallGeometry, wallMaterial); + wall.position.set(0, 0.5, -0.5); + scene.add(wall); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.shadowMap.enabled = true; + container.appendChild(renderer.domElement); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 0.1, 0); + controls.update(); + controls.minDistance = 0.5; + controls.maxDistance = 10; + controls.maxPolarAngle = 0.5 * Math.PI; + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webaudio_sandbox.ts b/examples-testing/examples/webaudio_sandbox.ts new file mode 100644 index 000000000..d67d0d552 --- /dev/null +++ b/examples-testing/examples/webaudio_sandbox.ts @@ -0,0 +1,222 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js'; + +let camera, controls, scene, renderer, light; + +let material1, material2, material3; + +let analyser1, analyser2, analyser3; + +const clock = new THREE.Clock(); + +const startButton = document.getElementById('startButton'); +startButton.addEventListener('click', init); + +function init() { + const overlay = document.getElementById('overlay'); + overlay.remove(); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.set(0, 25, 0); + + const listener = new THREE.AudioListener(); + camera.add(listener); + + scene = new THREE.Scene(); + scene.fog = new THREE.FogExp2(0x000000, 0.0025); + + light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0, 0.5, 1).normalize(); + scene.add(light); + + const sphere = new THREE.SphereGeometry(20, 32, 16); + + material1 = new THREE.MeshPhongMaterial({ color: 0xffaa00, flatShading: true, shininess: 0 }); + material2 = new THREE.MeshPhongMaterial({ color: 0xff2200, flatShading: true, shininess: 0 }); + material3 = new THREE.MeshPhongMaterial({ color: 0x6622aa, flatShading: true, shininess: 0 }); + + // sound spheres + + const mesh1 = new THREE.Mesh(sphere, material1); + mesh1.position.set(-250, 30, 0); + scene.add(mesh1); + + const sound1 = new THREE.PositionalAudio(listener); + const songElement = document.getElementById('song'); + sound1.setMediaElementSource(songElement); + sound1.setRefDistance(20); + songElement.play(); + mesh1.add(sound1); + + // + + const mesh2 = new THREE.Mesh(sphere, material2); + mesh2.position.set(250, 30, 0); + scene.add(mesh2); + + const sound2 = new THREE.PositionalAudio(listener); + const skullbeatzElement = document.getElementById('skullbeatz'); + sound2.setMediaElementSource(skullbeatzElement); + sound2.setRefDistance(20); + skullbeatzElement.play(); + mesh2.add(sound2); + + // + + const mesh3 = new THREE.Mesh(sphere, material3); + mesh3.position.set(0, 30, -250); + scene.add(mesh3); + + const sound3 = new THREE.PositionalAudio(listener); + const oscillator = listener.context.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(144, sound3.context.currentTime); + oscillator.start(0); + sound3.setNodeSource(oscillator); + sound3.setRefDistance(20); + sound3.setVolume(0.5); + mesh3.add(sound3); + + // analysers + + analyser1 = new THREE.AudioAnalyser(sound1, 32); + analyser2 = new THREE.AudioAnalyser(sound2, 32); + analyser3 = new THREE.AudioAnalyser(sound3, 32); + + // global ambient audio + + const sound4 = new THREE.Audio(listener); + const utopiaElement = document.getElementById('utopia'); + sound4.setMediaElementSource(utopiaElement); + sound4.setVolume(0.5); + utopiaElement.play(); + + // ground + + const helper = new THREE.GridHelper(1000, 10, 0x444444, 0x444444); + helper.position.y = 0.1; + scene.add(helper); + + // + + const SoundControls = function () { + this.master = listener.getMasterVolume(); + this.firstSphere = sound1.getVolume(); + this.secondSphere = sound2.getVolume(); + this.thirdSphere = sound3.getVolume(); + this.Ambient = sound4.getVolume(); + }; + + const GeneratorControls = function () { + this.frequency = oscillator.frequency.value; + this.wavetype = oscillator.type; + }; + + const gui = new GUI(); + const soundControls = new SoundControls(); + const generatorControls = new GeneratorControls(); + const volumeFolder = gui.addFolder('sound volume'); + const generatorFolder = gui.addFolder('sound generator'); + + volumeFolder + .add(soundControls, 'master') + .min(0.0) + .max(1.0) + .step(0.01) + .onChange(function () { + listener.setMasterVolume(soundControls.master); + }); + volumeFolder + .add(soundControls, 'firstSphere') + .min(0.0) + .max(1.0) + .step(0.01) + .onChange(function () { + sound1.setVolume(soundControls.firstSphere); + }); + volumeFolder + .add(soundControls, 'secondSphere') + .min(0.0) + .max(1.0) + .step(0.01) + .onChange(function () { + sound2.setVolume(soundControls.secondSphere); + }); + + volumeFolder + .add(soundControls, 'thirdSphere') + .min(0.0) + .max(1.0) + .step(0.01) + .onChange(function () { + sound3.setVolume(soundControls.thirdSphere); + }); + volumeFolder + .add(soundControls, 'Ambient') + .min(0.0) + .max(1.0) + .step(0.01) + .onChange(function () { + sound4.setVolume(soundControls.Ambient); + }); + volumeFolder.open(); + generatorFolder + .add(generatorControls, 'frequency') + .min(50.0) + .max(5000.0) + .step(1.0) + .onChange(function () { + oscillator.frequency.setValueAtTime(generatorControls.frequency, listener.context.currentTime); + }); + generatorFolder + .add(generatorControls, 'wavetype', ['sine', 'square', 'sawtooth', 'triangle']) + .onChange(function () { + oscillator.type = generatorControls.wavetype; + }); + + generatorFolder.open(); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + controls = new FirstPersonControls(camera, renderer.domElement); + + controls.movementSpeed = 70; + controls.lookSpeed = 0.05; + controls.lookVertical = false; + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + controls.handleResize(); +} + +function animate() { + const delta = clock.getDelta(); + + controls.update(delta); + + material1.emissive.b = analyser1.getAverageFrequency() / 256; + material2.emissive.b = analyser2.getAverageFrequency() / 256; + material3.emissive.b = analyser3.getAverageFrequency() / 256; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webaudio_timing.ts b/examples-testing/examples/webaudio_timing.ts new file mode 100644 index 000000000..9e17bcbcd --- /dev/null +++ b/examples-testing/examples/webaudio_timing.ts @@ -0,0 +1,152 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let scene, camera, renderer, clock; + +const objects = []; + +const speed = 2.5; +const height = 3; +const offset = 0.5; + +const startButton = document.getElementById('startButton'); +startButton.addEventListener('click', init); + +function init() { + const overlay = document.getElementById('overlay'); + overlay.remove(); + + const container = document.getElementById('container'); + + scene = new THREE.Scene(); + + clock = new THREE.Clock(); + + // + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(7, 3, 7); + + // lights + + const ambientLight = new THREE.AmbientLight(0xcccccc); + scene.add(ambientLight); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 2.5); + directionalLight.position.set(0, 5, 5); + scene.add(directionalLight); + + const d = 5; + directionalLight.castShadow = true; + directionalLight.shadow.camera.left = -d; + directionalLight.shadow.camera.right = d; + directionalLight.shadow.camera.top = d; + directionalLight.shadow.camera.bottom = -d; + + directionalLight.shadow.camera.near = 1; + directionalLight.shadow.camera.far = 20; + + directionalLight.shadow.mapSize.x = 1024; + directionalLight.shadow.mapSize.y = 1024; + + // audio + + const audioLoader = new THREE.AudioLoader(); + + const listener = new THREE.AudioListener(); + camera.add(listener); + + // floor + + const floorGeometry = new THREE.PlaneGeometry(10, 10); + const floorMaterial = new THREE.MeshLambertMaterial({ color: 0x4676b6 }); + + const floor = new THREE.Mesh(floorGeometry, floorMaterial); + floor.rotation.x = Math.PI * -0.5; + floor.receiveShadow = true; + scene.add(floor); + + // objects + + const count = 5; + const radius = 3; + + const ballGeometry = new THREE.SphereGeometry(0.3, 32, 16); + ballGeometry.translate(0, 0.3, 0); + const ballMaterial = new THREE.MeshLambertMaterial({ color: 0xcccccc }); + + // create objects when audio buffer is loaded + + audioLoader.load('sounds/ping_pong.mp3', function (buffer) { + for (let i = 0; i < count; i++) { + const s = (i / count) * Math.PI * 2; + + const ball = new THREE.Mesh(ballGeometry, ballMaterial); + ball.castShadow = true; + ball.userData.down = false; + + ball.position.x = radius * Math.cos(s); + ball.position.z = radius * Math.sin(s); + + const audio = new THREE.PositionalAudio(listener); + audio.setBuffer(buffer); + ball.add(audio); + + scene.add(ball); + objects.push(ball); + } + + renderer.setAnimationLoop(animate); + }); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.shadowMap.enabled = true; + container.appendChild(renderer.domElement); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 1; + controls.maxDistance = 25; + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const time = clock.getElapsedTime(); + + for (let i = 0; i < objects.length; i++) { + const ball = objects[i]; + + const previousHeight = ball.position.y; + ball.position.y = Math.abs(Math.sin(i * offset + time * speed) * height); + + if (ball.position.y < previousHeight) { + ball.userData.down = true; + } else { + if (ball.userData.down === true) { + // ball changed direction from down to up + + const audio = ball.children[0]; + audio.play(); // play audio with perfect timing when ball hits the surface + ball.userData.down = false; + } + } + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webaudio_visualizer.ts b/examples-testing/examples/webaudio_visualizer.ts new file mode 100644 index 000000000..a3f58cb36 --- /dev/null +++ b/examples-testing/examples/webaudio_visualizer.ts @@ -0,0 +1,86 @@ +import * as THREE from 'three'; + +let scene, camera, renderer, analyser, uniforms; + +const startButton = document.getElementById('startButton'); +startButton.addEventListener('click', init); + +function init() { + const fftSize = 128; + + // + + const overlay = document.getElementById('overlay'); + overlay.remove(); + + // + + const container = document.getElementById('container'); + + scene = new THREE.Scene(); + + camera = new THREE.Camera(); + + // + + const listener = new THREE.AudioListener(); + + const audio = new THREE.Audio(listener); + const file = './sounds/376737_Skullbeatz___Bad_Cat_Maste.mp3'; + + if (/(iPad|iPhone|iPod)/g.test(navigator.userAgent)) { + const loader = new THREE.AudioLoader(); + loader.load(file, function (buffer) { + audio.setBuffer(buffer); + audio.play(); + }); + } else { + const mediaElement = new Audio(file); + mediaElement.play(); + + audio.setMediaElementSource(mediaElement); + } + + analyser = new THREE.AudioAnalyser(audio, fftSize); + + // + + uniforms = { + tAudioData: { value: new THREE.DataTexture(analyser.data, fftSize / 2, 1, THREE.RedFormat) }, + }; + + const material = new THREE.ShaderMaterial({ + uniforms: uniforms, + vertexShader: document.getElementById('vertexShader').textContent, + fragmentShader: document.getElementById('fragmentShader').textContent, + }); + + const geometry = new THREE.PlaneGeometry(1, 1); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + analyser.getFrequencyData(); + + uniforms.tAudioData.value.needsUpdate = true; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_animation_keyframes.ts b/examples-testing/examples/webgl_animation_keyframes.ts new file mode 100644 index 000000000..88048f24c --- /dev/null +++ b/examples-testing/examples/webgl_animation_keyframes.ts @@ -0,0 +1,80 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; + +let mixer; + +const clock = new THREE.Clock(); +const container = document.getElementById('container'); + +const stats = new Stats(); +container.appendChild(stats.dom); + +const renderer = new THREE.WebGLRenderer({ antialias: true }); +renderer.setPixelRatio(window.devicePixelRatio); +renderer.setSize(window.innerWidth, window.innerHeight); +container.appendChild(renderer.domElement); + +const pmremGenerator = new THREE.PMREMGenerator(renderer); + +const scene = new THREE.Scene(); +scene.background = new THREE.Color(0xbfe3dd); +scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture; + +const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 100); +camera.position.set(5, 2, 8); + +const controls = new OrbitControls(camera, renderer.domElement); +controls.target.set(0, 0.5, 0); +controls.update(); +controls.enablePan = false; +controls.enableDamping = true; + +const dracoLoader = new DRACOLoader(); +dracoLoader.setDecoderPath('jsm/libs/draco/gltf/'); + +const loader = new GLTFLoader(); +loader.setDRACOLoader(dracoLoader); +loader.load( + 'models/gltf/LittlestTokyo.glb', + function (gltf) { + const model = gltf.scene; + model.position.set(1, 1, 0); + model.scale.set(0.01, 0.01, 0.01); + scene.add(model); + + mixer = new THREE.AnimationMixer(model); + mixer.clipAction(gltf.animations[0]).play(); + + renderer.setAnimationLoop(animate); + }, + undefined, + function (e) { + console.error(e); + }, +); + +window.onresize = function () { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +}; + +function animate() { + const delta = clock.getDelta(); + + mixer.update(delta); + + controls.update(); + + stats.update(); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_animation_multiple.ts b/examples-testing/examples/webgl_animation_multiple.ts new file mode 100644 index 000000000..152c65067 --- /dev/null +++ b/examples-testing/examples/webgl_animation_multiple.ts @@ -0,0 +1,197 @@ +import * as THREE from 'three'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, clock; +let model, animations; + +const mixers = [], + objects = []; + +const params = { + sharedSkeleton: false, +}; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(2, 3, -6); + camera.lookAt(0, 1, 0); + + clock = new THREE.Clock(); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xa0a0a0); + scene.fog = new THREE.Fog(0xa0a0a0, 10, 50); + + const hemiLight = new THREE.HemisphereLight(0xffffff, 0x8d8d8d, 3); + hemiLight.position.set(0, 20, 0); + scene.add(hemiLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.position.set(-3, 10, -10); + dirLight.castShadow = true; + dirLight.shadow.camera.top = 4; + dirLight.shadow.camera.bottom = -4; + dirLight.shadow.camera.left = -4; + dirLight.shadow.camera.right = 4; + dirLight.shadow.camera.near = 0.1; + dirLight.shadow.camera.far = 40; + scene.add(dirLight); + + // scene.add( new THREE.CameraHelper( dirLight.shadow.camera ) ); + + // ground + + const mesh = new THREE.Mesh( + new THREE.PlaneGeometry(200, 200), + new THREE.MeshPhongMaterial({ color: 0xcbcbcb, depthWrite: false }), + ); + mesh.rotation.x = -Math.PI / 2; + mesh.receiveShadow = true; + scene.add(mesh); + + const loader = new GLTFLoader(); + loader.load('models/gltf/Soldier.glb', function (gltf) { + model = gltf.scene; + animations = gltf.animations; + + model.traverse(function (object) { + if (object.isMesh) object.castShadow = true; + }); + + setupDefaultScene(); + }); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + document.body.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); + + const gui = new GUI(); + + gui.add(params, 'sharedSkeleton').onChange(function () { + clearScene(); + + if (params.sharedSkeleton === true) { + setupSharedSkeletonScene(); + } else { + setupDefaultScene(); + } + }); + gui.open(); +} + +function clearScene() { + for (const mixer of mixers) { + mixer.stopAllAction(); + } + + mixers.length = 0; + + // + + for (const object of objects) { + scene.remove(object); + + scene.traverse(function (child) { + if (child.isSkinnedMesh) child.skeleton.dispose(); + }); + } +} + +function setupDefaultScene() { + // three cloned models with independent skeletons. + // each model can have its own animation state + + const model1 = SkeletonUtils.clone(model); + const model2 = SkeletonUtils.clone(model); + const model3 = SkeletonUtils.clone(model); + + model1.position.x = -2; + model2.position.x = 0; + model3.position.x = 2; + + const mixer1 = new THREE.AnimationMixer(model1); + const mixer2 = new THREE.AnimationMixer(model2); + const mixer3 = new THREE.AnimationMixer(model3); + + mixer1.clipAction(animations[0]).play(); // idle + mixer2.clipAction(animations[1]).play(); // run + mixer3.clipAction(animations[3]).play(); // walk + + scene.add(model1, model2, model3); + + objects.push(model1, model2, model3); + mixers.push(mixer1, mixer2, mixer3); +} + +function setupSharedSkeletonScene() { + // three cloned models with a single shared skeleton. + // all models share the same animation state + + const sharedModel = SkeletonUtils.clone(model); + const shareSkinnedMesh = sharedModel.getObjectByName('vanguard_Mesh'); + const sharedSkeleton = shareSkinnedMesh.skeleton; + const sharedParentBone = sharedModel.getObjectByName('mixamorigHips'); + scene.add(sharedParentBone); // the bones need to be in the scene for the animation to work + + const model1 = shareSkinnedMesh.clone(); + const model2 = shareSkinnedMesh.clone(); + const model3 = shareSkinnedMesh.clone(); + + model1.bindMode = THREE.DetachedBindMode; + model2.bindMode = THREE.DetachedBindMode; + model3.bindMode = THREE.DetachedBindMode; + + const identity = new THREE.Matrix4(); + + model1.bind(sharedSkeleton, identity); + model2.bind(sharedSkeleton, identity); + model3.bind(sharedSkeleton, identity); + + model1.position.x = -2; + model2.position.x = 0; + model3.position.x = 2; + + // apply transformation from the glTF asset + + model1.scale.setScalar(0.01); + model1.rotation.x = -Math.PI * 0.5; + model2.scale.setScalar(0.01); + model2.rotation.x = -Math.PI * 0.5; + model3.scale.setScalar(0.01); + model3.rotation.x = -Math.PI * 0.5; + + // + + const mixer = new THREE.AnimationMixer(sharedParentBone); + mixer.clipAction(animations[1]).play(); + + scene.add(sharedParentBone, model1, model2, model3); + + objects.push(sharedParentBone, model1, model2, model3); + mixers.push(mixer); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const delta = clock.getDelta(); + + for (const mixer of mixers) mixer.update(delta); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_animation_skinning_morph.ts b/examples-testing/examples/webgl_animation_skinning_morph.ts new file mode 100644 index 000000000..f05369aa9 --- /dev/null +++ b/examples-testing/examples/webgl_animation_skinning_morph.ts @@ -0,0 +1,187 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +let container, stats, clock, gui, mixer, actions, activeAction, previousAction; +let camera, scene, renderer, model, face; + +const api = { state: 'Walking' }; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 100); + camera.position.set(-5, 3, 10); + camera.lookAt(0, 2, 0); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xe0e0e0); + scene.fog = new THREE.Fog(0xe0e0e0, 20, 100); + + clock = new THREE.Clock(); + + // lights + + const hemiLight = new THREE.HemisphereLight(0xffffff, 0x8d8d8d, 3); + hemiLight.position.set(0, 20, 0); + scene.add(hemiLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.position.set(0, 20, 10); + scene.add(dirLight); + + // ground + + const mesh = new THREE.Mesh( + new THREE.PlaneGeometry(2000, 2000), + new THREE.MeshPhongMaterial({ color: 0xcbcbcb, depthWrite: false }), + ); + mesh.rotation.x = -Math.PI / 2; + scene.add(mesh); + + const grid = new THREE.GridHelper(200, 40, 0x000000, 0x000000); + grid.material.opacity = 0.2; + grid.material.transparent = true; + scene.add(grid); + + // model + + const loader = new GLTFLoader(); + loader.load( + 'models/gltf/RobotExpressive/RobotExpressive.glb', + function (gltf) { + model = gltf.scene; + scene.add(model); + + createGUI(model, gltf.animations); + }, + undefined, + function (e) { + console.error(e); + }, + ); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); + + // stats + stats = new Stats(); + container.appendChild(stats.dom); +} + +function createGUI(model, animations) { + const states = ['Idle', 'Walking', 'Running', 'Dance', 'Death', 'Sitting', 'Standing']; + const emotes = ['Jump', 'Yes', 'No', 'Wave', 'Punch', 'ThumbsUp']; + + gui = new GUI(); + + mixer = new THREE.AnimationMixer(model); + + actions = {}; + + for (let i = 0; i < animations.length; i++) { + const clip = animations[i]; + const action = mixer.clipAction(clip); + actions[clip.name] = action; + + if (emotes.indexOf(clip.name) >= 0 || states.indexOf(clip.name) >= 4) { + action.clampWhenFinished = true; + action.loop = THREE.LoopOnce; + } + } + + // states + + const statesFolder = gui.addFolder('States'); + + const clipCtrl = statesFolder.add(api, 'state').options(states); + + clipCtrl.onChange(function () { + fadeToAction(api.state, 0.5); + }); + + statesFolder.open(); + + // emotes + + const emoteFolder = gui.addFolder('Emotes'); + + function createEmoteCallback(name) { + api[name] = function () { + fadeToAction(name, 0.2); + + mixer.addEventListener('finished', restoreState); + }; + + emoteFolder.add(api, name); + } + + function restoreState() { + mixer.removeEventListener('finished', restoreState); + + fadeToAction(api.state, 0.2); + } + + for (let i = 0; i < emotes.length; i++) { + createEmoteCallback(emotes[i]); + } + + emoteFolder.open(); + + // expressions + + face = model.getObjectByName('Head_4'); + + const expressions = Object.keys(face.morphTargetDictionary); + const expressionFolder = gui.addFolder('Expressions'); + + for (let i = 0; i < expressions.length; i++) { + expressionFolder.add(face.morphTargetInfluences, i, 0, 1, 0.01).name(expressions[i]); + } + + activeAction = actions['Walking']; + activeAction.play(); + + expressionFolder.open(); +} + +function fadeToAction(name, duration) { + previousAction = activeAction; + activeAction = actions[name]; + + if (previousAction !== activeAction) { + previousAction.fadeOut(duration); + } + + activeAction.reset().setEffectiveTimeScale(1).setEffectiveWeight(1).fadeIn(duration).play(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const dt = clock.getDelta(); + + if (mixer) mixer.update(dt); + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_buffergeometry.ts b/examples-testing/examples/webgl_buffergeometry.ts new file mode 100644 index 000000000..28b2c96a4 --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry.ts @@ -0,0 +1,178 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; + +let camera, scene, renderer; + +let mesh; + +init(); +animate(); + +function init() { + container = document.getElementById('container'); + + // + + camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 1, 3500); + camera.position.z = 2750; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050505); + scene.fog = new THREE.Fog(0x050505, 2000, 3500); + + // + + scene.add(new THREE.AmbientLight(0xcccccc)); + + const light1 = new THREE.DirectionalLight(0xffffff, 1.5); + light1.position.set(1, 1, 1); + scene.add(light1); + + const light2 = new THREE.DirectionalLight(0xffffff, 4.5); + light2.position.set(0, -1, 0); + scene.add(light2); + + // + + const triangles = 160000; + + const geometry = new THREE.BufferGeometry(); + + const positions = []; + const normals = []; + const colors = []; + + const color = new THREE.Color(); + + const n = 800, + n2 = n / 2; // triangles spread in the cube + const d = 12, + d2 = d / 2; // individual triangle size + + const pA = new THREE.Vector3(); + const pB = new THREE.Vector3(); + const pC = new THREE.Vector3(); + + const cb = new THREE.Vector3(); + const ab = new THREE.Vector3(); + + for (let i = 0; i < triangles; i++) { + // positions + + const x = Math.random() * n - n2; + const y = Math.random() * n - n2; + const z = Math.random() * n - n2; + + const ax = x + Math.random() * d - d2; + const ay = y + Math.random() * d - d2; + const az = z + Math.random() * d - d2; + + const bx = x + Math.random() * d - d2; + const by = y + Math.random() * d - d2; + const bz = z + Math.random() * d - d2; + + const cx = x + Math.random() * d - d2; + const cy = y + Math.random() * d - d2; + const cz = z + Math.random() * d - d2; + + positions.push(ax, ay, az); + positions.push(bx, by, bz); + positions.push(cx, cy, cz); + + // flat face normals + + pA.set(ax, ay, az); + pB.set(bx, by, bz); + pC.set(cx, cy, cz); + + cb.subVectors(pC, pB); + ab.subVectors(pA, pB); + cb.cross(ab); + + cb.normalize(); + + const nx = cb.x; + const ny = cb.y; + const nz = cb.z; + + normals.push(nx, ny, nz); + normals.push(nx, ny, nz); + normals.push(nx, ny, nz); + + // colors + + const vx = x / n + 0.5; + const vy = y / n + 0.5; + const vz = z / n + 0.5; + + color.setRGB(vx, vy, vz); + + const alpha = Math.random(); + + colors.push(color.r, color.g, color.b, alpha); + colors.push(color.r, color.g, color.b, alpha); + colors.push(color.r, color.g, color.b, alpha); + } + + function disposeArray() { + this.array = null; + } + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3).onUpload(disposeArray)); + geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3).onUpload(disposeArray)); + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 4).onUpload(disposeArray)); + + geometry.computeBoundingSphere(); + + const material = new THREE.MeshPhongMaterial({ + color: 0xd5d5d5, + specular: 0xffffff, + shininess: 250, + side: THREE.DoubleSide, + vertexColors: true, + transparent: true, + }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const time = Date.now() * 0.001; + + mesh.rotation.x = time * 0.25; + mesh.rotation.y = time * 0.5; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_buffergeometry_attributes_integer.ts b/examples-testing/examples/webgl_buffergeometry_attributes_integer.ts new file mode 100644 index 000000000..96926c2c3 --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_attributes_integer.ts @@ -0,0 +1,113 @@ +import * as THREE from 'three'; + +let camera, scene, renderer, mesh; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 1, 3500); + camera.position.z = 2500; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050505); + scene.fog = new THREE.Fog(0x050505, 2000, 3500); + + // geometry + + const triangles = 10000; + + const geometry = new THREE.BufferGeometry(); + + const positions = []; + const uvs = []; + const textureIndices = []; + + const n = 800, + n2 = n / 2; // triangles spread in the cube + const d = 50, + d2 = d / 2; // individual triangle size + + for (let i = 0; i < triangles; i++) { + // positions + + const x = Math.random() * n - n2; + const y = Math.random() * n - n2; + const z = Math.random() * n - n2; + + const ax = x + Math.random() * d - d2; + const ay = y + Math.random() * d - d2; + const az = z + Math.random() * d - d2; + + const bx = x + Math.random() * d - d2; + const by = y + Math.random() * d - d2; + const bz = z + Math.random() * d - d2; + + const cx = x + Math.random() * d - d2; + const cy = y + Math.random() * d - d2; + const cz = z + Math.random() * d - d2; + + positions.push(ax, ay, az); + positions.push(bx, by, bz); + positions.push(cx, cy, cz); + + // uvs + + uvs.push(0, 0); + uvs.push(0.5, 1); + uvs.push(1, 0); + + // texture indices + + const t = i % 3; + textureIndices.push(t, t, t); + } + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2)); + geometry.setAttribute('textureIndex', new THREE.Int16BufferAttribute(textureIndices, 1)); + geometry.attributes.textureIndex.gpuType = THREE.IntType; + + geometry.computeBoundingSphere(); + + // material + + const loader = new THREE.TextureLoader(); + + const map1 = loader.load('textures/crate.gif'); + const map2 = loader.load('textures/floors/FloorsCheckerboard_S_Diffuse.jpg'); + const map3 = loader.load('textures/terrain/grasslight-big.jpg'); + + const material = new THREE.ShaderMaterial({ + uniforms: { + uTextures: { + value: [map1, map2, map3], + }, + }, + vertexShader: document.getElementById('vertexShader').textContent, + fragmentShader: document.getElementById('fragmentShader').textContent, + side: THREE.DoubleSide, + glslVersion: THREE.GLSL3, + }); + + // mesh + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); +} + +function animate() { + const time = Date.now() * 0.001; + + mesh.rotation.x = time * 0.25; + mesh.rotation.y = time * 0.5; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_buffergeometry_attributes_none.ts b/examples-testing/examples/webgl_buffergeometry_attributes_none.ts new file mode 100644 index 000000000..a1424e871 --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_attributes_none.ts @@ -0,0 +1,56 @@ +import * as THREE from 'three'; + +let camera, scene, renderer, mesh; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 1, 3500); + camera.position.z = 4; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050505); + scene.fog = new THREE.Fog(0x050505, 2000, 3500); + + // geometry + + const triangleCount = 10000; + const vertexCountPerTriangle = 3; + const vertexCount = triangleCount * vertexCountPerTriangle; + + const geometry = new THREE.BufferGeometry(); + geometry.setDrawRange(0, vertexCount); + + // material + + const material = new THREE.RawShaderMaterial({ + uniforms: { + seed: { value: 42 }, + }, + vertexShader: document.getElementById('vertexShader').textContent, + fragmentShader: document.getElementById('fragmentShader').textContent, + side: THREE.DoubleSide, + glslVersion: THREE.GLSL3, + }); + + // mesh + + mesh = new THREE.Mesh(geometry, material); + mesh.frustumCulled = false; + scene.add(mesh); + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); +} + +function animate(time) { + mesh.rotation.x = (time / 1000.0) * 0.25; + mesh.rotation.y = (time / 1000.0) * 0.5; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_buffergeometry_custom_attributes_particles.ts b/examples-testing/examples/webgl_buffergeometry_custom_attributes_particles.ts new file mode 100644 index 000000000..0dffa65cc --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_custom_attributes_particles.ts @@ -0,0 +1,103 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let renderer, scene, camera, stats; + +let particleSystem, uniforms, geometry; + +const particles = 100000; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 300; + + scene = new THREE.Scene(); + + uniforms = { + pointTexture: { value: new THREE.TextureLoader().load('textures/sprites/spark1.png') }, + }; + + const shaderMaterial = new THREE.ShaderMaterial({ + uniforms: uniforms, + vertexShader: document.getElementById('vertexshader').textContent, + fragmentShader: document.getElementById('fragmentshader').textContent, + + blending: THREE.AdditiveBlending, + depthTest: false, + transparent: true, + vertexColors: true, + }); + + const radius = 200; + + geometry = new THREE.BufferGeometry(); + + const positions = []; + const colors = []; + const sizes = []; + + const color = new THREE.Color(); + + for (let i = 0; i < particles; i++) { + positions.push((Math.random() * 2 - 1) * radius); + positions.push((Math.random() * 2 - 1) * radius); + positions.push((Math.random() * 2 - 1) * radius); + + color.setHSL(i / particles, 1.0, 0.5); + + colors.push(color.r, color.g, color.b); + + sizes.push(20); + } + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + geometry.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1).setUsage(THREE.DynamicDrawUsage)); + + particleSystem = new THREE.Points(geometry, shaderMaterial); + + scene.add(particleSystem); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + + const container = document.getElementById('container'); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const time = Date.now() * 0.005; + + particleSystem.rotation.z = 0.01 * time; + + const sizes = geometry.attributes.size.array; + + for (let i = 0; i < particles; i++) { + sizes[i] = 10 * (1 + Math.sin(0.1 * i + time)); + } + + geometry.attributes.size.needsUpdate = true; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_buffergeometry_drawrange.ts b/examples-testing/examples/webgl_buffergeometry_drawrange.ts new file mode 100644 index 000000000..142ff43bf --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_drawrange.ts @@ -0,0 +1,239 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let group; +let container, stats; +const particlesData = []; +let camera, scene, renderer; +let positions, colors; +let particles; +let pointCloud; +let particlePositions; +let linesMesh; + +const maxParticleCount = 1000; +let particleCount = 500; +const r = 800; +const rHalf = r / 2; + +const effectController = { + showDots: true, + showLines: true, + minDistance: 150, + limitConnections: false, + maxConnections: 20, + particleCount: 500, +}; + +init(); + +function initGUI() { + const gui = new GUI(); + + gui.add(effectController, 'showDots').onChange(function (value) { + pointCloud.visible = value; + }); + gui.add(effectController, 'showLines').onChange(function (value) { + linesMesh.visible = value; + }); + gui.add(effectController, 'minDistance', 10, 300); + gui.add(effectController, 'limitConnections'); + gui.add(effectController, 'maxConnections', 0, 30, 1); + gui.add(effectController, 'particleCount', 0, maxParticleCount, 1).onChange(function (value) { + particleCount = value; + particles.setDrawRange(0, particleCount); + }); +} + +function init() { + initGUI(); + + container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 4000); + camera.position.z = 1750; + + const controls = new OrbitControls(camera, container); + controls.minDistance = 1000; + controls.maxDistance = 3000; + + scene = new THREE.Scene(); + + group = new THREE.Group(); + scene.add(group); + + const helper = new THREE.BoxHelper(new THREE.Mesh(new THREE.BoxGeometry(r, r, r))); + helper.material.color.setHex(0x474747); + helper.material.blending = THREE.AdditiveBlending; + helper.material.transparent = true; + group.add(helper); + + const segments = maxParticleCount * maxParticleCount; + + positions = new Float32Array(segments * 3); + colors = new Float32Array(segments * 3); + + const pMaterial = new THREE.PointsMaterial({ + color: 0xffffff, + size: 3, + blending: THREE.AdditiveBlending, + transparent: true, + sizeAttenuation: false, + }); + + particles = new THREE.BufferGeometry(); + particlePositions = new Float32Array(maxParticleCount * 3); + + for (let i = 0; i < maxParticleCount; i++) { + const x = Math.random() * r - r / 2; + const y = Math.random() * r - r / 2; + const z = Math.random() * r - r / 2; + + particlePositions[i * 3] = x; + particlePositions[i * 3 + 1] = y; + particlePositions[i * 3 + 2] = z; + + // add it to the geometry + particlesData.push({ + velocity: new THREE.Vector3(-1 + Math.random() * 2, -1 + Math.random() * 2, -1 + Math.random() * 2), + numConnections: 0, + }); + } + + particles.setDrawRange(0, particleCount); + particles.setAttribute( + 'position', + new THREE.BufferAttribute(particlePositions, 3).setUsage(THREE.DynamicDrawUsage), + ); + + // create the particle system + pointCloud = new THREE.Points(particles, pMaterial); + group.add(pointCloud); + + const geometry = new THREE.BufferGeometry(); + + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3).setUsage(THREE.DynamicDrawUsage)); + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3).setUsage(THREE.DynamicDrawUsage)); + + geometry.computeBoundingSphere(); + + geometry.setDrawRange(0, 0); + + const material = new THREE.LineBasicMaterial({ + vertexColors: true, + blending: THREE.AdditiveBlending, + transparent: true, + }); + + linesMesh = new THREE.LineSegments(geometry, material); + group.add(linesMesh); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + let vertexpos = 0; + let colorpos = 0; + let numConnected = 0; + + for (let i = 0; i < particleCount; i++) particlesData[i].numConnections = 0; + + for (let i = 0; i < particleCount; i++) { + // get the particle + const particleData = particlesData[i]; + + particlePositions[i * 3] += particleData.velocity.x; + particlePositions[i * 3 + 1] += particleData.velocity.y; + particlePositions[i * 3 + 2] += particleData.velocity.z; + + if (particlePositions[i * 3 + 1] < -rHalf || particlePositions[i * 3 + 1] > rHalf) + particleData.velocity.y = -particleData.velocity.y; + + if (particlePositions[i * 3] < -rHalf || particlePositions[i * 3] > rHalf) + particleData.velocity.x = -particleData.velocity.x; + + if (particlePositions[i * 3 + 2] < -rHalf || particlePositions[i * 3 + 2] > rHalf) + particleData.velocity.z = -particleData.velocity.z; + + if (effectController.limitConnections && particleData.numConnections >= effectController.maxConnections) + continue; + + // Check collision + for (let j = i + 1; j < particleCount; j++) { + const particleDataB = particlesData[j]; + if (effectController.limitConnections && particleDataB.numConnections >= effectController.maxConnections) + continue; + + const dx = particlePositions[i * 3] - particlePositions[j * 3]; + const dy = particlePositions[i * 3 + 1] - particlePositions[j * 3 + 1]; + const dz = particlePositions[i * 3 + 2] - particlePositions[j * 3 + 2]; + const dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist < effectController.minDistance) { + particleData.numConnections++; + particleDataB.numConnections++; + + const alpha = 1.0 - dist / effectController.minDistance; + + positions[vertexpos++] = particlePositions[i * 3]; + positions[vertexpos++] = particlePositions[i * 3 + 1]; + positions[vertexpos++] = particlePositions[i * 3 + 2]; + + positions[vertexpos++] = particlePositions[j * 3]; + positions[vertexpos++] = particlePositions[j * 3 + 1]; + positions[vertexpos++] = particlePositions[j * 3 + 2]; + + colors[colorpos++] = alpha; + colors[colorpos++] = alpha; + colors[colorpos++] = alpha; + + colors[colorpos++] = alpha; + colors[colorpos++] = alpha; + colors[colorpos++] = alpha; + + numConnected++; + } + } + } + + linesMesh.geometry.setDrawRange(0, numConnected * 2); + linesMesh.geometry.attributes.position.needsUpdate = true; + linesMesh.geometry.attributes.color.needsUpdate = true; + + pointCloud.geometry.attributes.position.needsUpdate = true; + + render(); + + stats.update(); +} + +function render() { + const time = Date.now() * 0.001; + + group.rotation.y = time * 0.1; + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_buffergeometry_glbufferattribute.ts b/examples-testing/examples/webgl_buffergeometry_glbufferattribute.ts new file mode 100644 index 000000000..aea462cf4 --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_glbufferattribute.ts @@ -0,0 +1,139 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; + +let camera, scene, renderer; + +let points; + +const particles = 300000; +let drawCount = 10000; + +init(); +animate(); + +function init() { + container = document.getElementById('container'); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + + container.appendChild(renderer.domElement); + + // + + camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 5, 3500); + camera.position.z = 2750; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050505); + scene.fog = new THREE.Fog(0x050505, 2000, 3500); + + // + + const geometry = new THREE.BufferGeometry(); + + const positions = []; + const positions2 = []; + const colors = []; + + const color = new THREE.Color(); + + const n = 1000, + n2 = n / 2; // particles spread in the cube + + for (let i = 0; i < particles; i++) { + // positions + + const x = Math.random() * n - n2; + const y = Math.random() * n - n2; + const z = Math.random() * n - n2; + + positions.push(x, y, z); + positions2.push(z * 0.3, x * 0.3, y * 0.3); + + // colors + + const vx = x / n + 0.5; + const vy = y / n + 0.5; + const vz = z / n + 0.5; + + color.setRGB(vx, vy, vz, THREE.SRGBColorSpace); + + colors.push(color.r, color.g, color.b); + } + + const gl = renderer.getContext(); + + const pos = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, pos); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); + + const pos2 = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, pos2); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions2), gl.STATIC_DRAW); + + const rgb = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, rgb); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW); + + const posAttr1 = new THREE.GLBufferAttribute(pos, gl.FLOAT, 3, 4, particles); + const posAttr2 = new THREE.GLBufferAttribute(pos2, gl.FLOAT, 3, 4, particles); + geometry.setAttribute('position', posAttr1); + + setInterval(function () { + const attr = geometry.getAttribute('position'); + + geometry.setAttribute('position', attr === posAttr1 ? posAttr2 : posAttr1); + }, 2000); + + geometry.setAttribute('color', new THREE.GLBufferAttribute(rgb, gl.FLOAT, 3, 4, particles)); + + // + + const material = new THREE.PointsMaterial({ size: 15, vertexColors: true }); + + points = new THREE.Points(geometry, material); + + geometry.boundingSphere = new THREE.Sphere().set(new THREE.Vector3(), 500); + + scene.add(points); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + drawCount = (Math.max(5000, drawCount) + Math.floor(500 * Math.random())) % particles; + points.geometry.setDrawRange(0, drawCount); + + const time = Date.now() * 0.001; + + points.rotation.x = time * 0.1; + points.rotation.y = time * 0.2; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_buffergeometry_indexed.ts b/examples-testing/examples/webgl_buffergeometry_indexed.ts new file mode 100644 index 000000000..a2f9f3795 --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_indexed.ts @@ -0,0 +1,137 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, stats; + +let mesh; + +init(); + +function init() { + // + + camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 1, 3500); + camera.position.z = 64; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050505); + + // + + const light = new THREE.HemisphereLight(); + light.intensity = 3; + scene.add(light); + + // + + const geometry = new THREE.BufferGeometry(); + + const indices = []; + + const vertices = []; + const normals = []; + const colors = []; + + const size = 20; + const segments = 10; + + const halfSize = size / 2; + const segmentSize = size / segments; + + const _color = new THREE.Color(); + + // generate vertices, normals and color data for a simple grid geometry + + for (let i = 0; i <= segments; i++) { + const y = i * segmentSize - halfSize; + + for (let j = 0; j <= segments; j++) { + const x = j * segmentSize - halfSize; + + vertices.push(x, -y, 0); + normals.push(0, 0, 1); + + const r = x / size + 0.5; + const g = y / size + 0.5; + + _color.setRGB(r, g, 1, THREE.SRGBColorSpace); + + colors.push(_color.r, _color.g, _color.b); + } + } + + // generate indices (data for element array buffer) + + for (let i = 0; i < segments; i++) { + for (let j = 0; j < segments; j++) { + const a = i * (segments + 1) + (j + 1); + const b = i * (segments + 1) + j; + const c = (i + 1) * (segments + 1) + j; + const d = (i + 1) * (segments + 1) + (j + 1); + + // generate two faces (triangles) per iteration + + indices.push(a, b, d); // face one + indices.push(b, c, d); // face two + } + } + + // + + geometry.setIndex(indices); + geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3)); + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + + const material = new THREE.MeshPhongMaterial({ + side: THREE.DoubleSide, + vertexColors: true, + }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + const gui = new GUI(); + gui.add(material, 'wireframe'); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const time = Date.now() * 0.001; + + mesh.rotation.x = time * 0.25; + mesh.rotation.y = time * 0.5; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_buffergeometry_instancing.ts b/examples-testing/examples/webgl_buffergeometry_instancing.ts new file mode 100644 index 000000000..a5b90ae6e --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_instancing.ts @@ -0,0 +1,138 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let container, stats; + +let camera, scene, renderer; + +init(); + +function init() { + container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10); + camera.position.z = 2; + + scene = new THREE.Scene(); + + // geometry + + const vector = new THREE.Vector4(); + + const instances = 50000; + + const positions = []; + const offsets = []; + const colors = []; + const orientationsStart = []; + const orientationsEnd = []; + + positions.push(0.025, -0.025, 0); + positions.push(-0.025, 0.025, 0); + positions.push(0, 0, 0.025); + + // instanced attributes + + for (let i = 0; i < instances; i++) { + // offsets + + offsets.push(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5); + + // colors + + colors.push(Math.random(), Math.random(), Math.random(), Math.random()); + + // orientation start + + vector.set(Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1); + vector.normalize(); + + orientationsStart.push(vector.x, vector.y, vector.z, vector.w); + + // orientation end + + vector.set(Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1); + vector.normalize(); + + orientationsEnd.push(vector.x, vector.y, vector.z, vector.w); + } + + const geometry = new THREE.InstancedBufferGeometry(); + geometry.instanceCount = instances; // set so its initalized for dat.GUI, will be set in first draw otherwise + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + + geometry.setAttribute('offset', new THREE.InstancedBufferAttribute(new Float32Array(offsets), 3)); + geometry.setAttribute('color', new THREE.InstancedBufferAttribute(new Float32Array(colors), 4)); + geometry.setAttribute( + 'orientationStart', + new THREE.InstancedBufferAttribute(new Float32Array(orientationsStart), 4), + ); + geometry.setAttribute('orientationEnd', new THREE.InstancedBufferAttribute(new Float32Array(orientationsEnd), 4)); + + // material + + const material = new THREE.RawShaderMaterial({ + uniforms: { + time: { value: 1.0 }, + sineTime: { value: 1.0 }, + }, + vertexShader: document.getElementById('vertexShader').textContent, + fragmentShader: document.getElementById('fragmentShader').textContent, + side: THREE.DoubleSide, + forceSinglePass: true, + transparent: true, + }); + + // + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + const gui = new GUI({ width: 350 }); + gui.add(geometry, 'instanceCount', 0, instances); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const time = performance.now(); + + const object = scene.children[0]; + + object.rotation.y = time * 0.0005; + object.material.uniforms['time'].value = time * 0.005; + object.material.uniforms['sineTime'].value = Math.sin(object.material.uniforms['time'].value * 0.05); + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_buffergeometry_instancing_billboards.ts b/examples-testing/examples/webgl_buffergeometry_instancing_billboards.ts new file mode 100644 index 000000000..2158dff39 --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_instancing_billboards.ts @@ -0,0 +1,86 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; + +let camera, scene, renderer; +let geometry, material, mesh; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 5000); + camera.position.z = 1400; + + scene = new THREE.Scene(); + + const circleGeometry = new THREE.CircleGeometry(1, 6); + + geometry = new THREE.InstancedBufferGeometry(); + geometry.index = circleGeometry.index; + geometry.attributes = circleGeometry.attributes; + + const particleCount = 75000; + + const translateArray = new Float32Array(particleCount * 3); + + for (let i = 0, i3 = 0, l = particleCount; i < l; i++, i3 += 3) { + translateArray[i3 + 0] = Math.random() * 2 - 1; + translateArray[i3 + 1] = Math.random() * 2 - 1; + translateArray[i3 + 2] = Math.random() * 2 - 1; + } + + geometry.setAttribute('translate', new THREE.InstancedBufferAttribute(translateArray, 3)); + + material = new THREE.RawShaderMaterial({ + uniforms: { + map: { value: new THREE.TextureLoader().load('textures/sprites/circle.png') }, + time: { value: 0.0 }, + }, + vertexShader: document.getElementById('vshader').textContent, + fragmentShader: document.getElementById('fshader').textContent, + depthTest: true, + depthWrite: true, + }); + + mesh = new THREE.Mesh(geometry, material); + mesh.scale.set(500, 500, 500); + scene.add(mesh); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); + + return true; +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const time = performance.now() * 0.0005; + + material.uniforms['time'].value = time; + + mesh.rotation.x = time * 0.2; + mesh.rotation.y = time * 0.4; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_buffergeometry_instancing_interleaved.ts b/examples-testing/examples/webgl_buffergeometry_instancing_interleaved.ts new file mode 100644 index 000000000..bef2c264d --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_instancing_interleaved.ts @@ -0,0 +1,152 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; +let camera, scene, renderer, mesh; + +const instances = 5000; +let lastTime = 0; + +const moveQ = new THREE.Quaternion(0.5, 0.5, 0.5, 0.0).normalize(); +const tmpQ = new THREE.Quaternion(); +const tmpM = new THREE.Matrix4(); +const currentM = new THREE.Matrix4(); + +init(); + +function init() { + container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x101010); + + // geometry + + const geometry = new THREE.InstancedBufferGeometry(); + + // per mesh data x,y,z,w,u,v,s,t for 4-element alignment + // only use x,y,z and u,v; but x, y, z, nx, ny, nz, u, v would be a good layout + const vertexBuffer = new THREE.InterleavedBuffer( + new Float32Array([ + // Front + -1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, -1, -1, 1, 0, 0, 1, 0, 0, 1, -1, 1, 0, 1, 1, 0, 0, + // Back + 1, 1, -1, 0, 1, 0, 0, 0, -1, 1, -1, 0, 0, 0, 0, 0, 1, -1, -1, 0, 1, 1, 0, 0, -1, -1, -1, 0, 0, 1, 0, 0, + // Left + -1, 1, -1, 0, 1, 1, 0, 0, -1, 1, 1, 0, 1, 0, 0, 0, -1, -1, -1, 0, 0, 1, 0, 0, -1, -1, 1, 0, 0, 0, 0, 0, + // Right + 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, -1, 0, 1, 1, 0, 0, 1, -1, 1, 0, 0, 0, 0, 0, 1, -1, -1, 0, 0, 1, 0, 0, + // Top + -1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, -1, 1, -1, 0, 0, 1, 0, 0, 1, 1, -1, 0, 1, 1, 0, 0, + // Bottom + 1, -1, 1, 0, 1, 0, 0, 0, -1, -1, 1, 0, 0, 0, 0, 0, 1, -1, -1, 0, 1, 1, 0, 0, -1, -1, -1, 0, 0, 1, 0, 0, + ]), + 8, + ); + + // Use vertexBuffer, starting at offset 0, 3 items in position attribute + const positions = new THREE.InterleavedBufferAttribute(vertexBuffer, 3, 0); + geometry.setAttribute('position', positions); + // Use vertexBuffer, starting at offset 4, 2 items in uv attribute + const uvs = new THREE.InterleavedBufferAttribute(vertexBuffer, 2, 4); + geometry.setAttribute('uv', uvs); + + const indices = new Uint16Array([ + 0, 2, 1, 2, 3, 1, 4, 6, 5, 6, 7, 5, 8, 10, 9, 10, 11, 9, 12, 14, 13, 14, 15, 13, 16, 17, 18, 18, 17, 19, 20, 21, + 22, 22, 21, 23, + ]); + + geometry.setIndex(new THREE.BufferAttribute(indices, 1)); + + // material + + const material = new THREE.MeshBasicMaterial(); + material.map = new THREE.TextureLoader().load('textures/crate.gif'); + material.map.colorSpace = THREE.SRGBColorSpace; + material.map.flipY = false; + + // per instance data + + const matrix = new THREE.Matrix4(); + const offset = new THREE.Vector3(); + const orientation = new THREE.Quaternion(); + const scale = new THREE.Vector3(1, 1, 1); + let x, y, z, w; + + mesh = new THREE.InstancedMesh(geometry, material, instances); + + for (let i = 0; i < instances; i++) { + // offsets + + x = Math.random() * 100 - 50; + y = Math.random() * 100 - 50; + z = Math.random() * 100 - 50; + + offset.set(x, y, z).normalize(); + offset.multiplyScalar(5); // move out at least 5 units from center in current direction + offset.set(x + offset.x, y + offset.y, z + offset.z); + + // orientations + + x = Math.random() * 2 - 1; + y = Math.random() * 2 - 1; + z = Math.random() * 2 - 1; + w = Math.random() * 2 - 1; + + orientation.set(x, y, z, w).normalize(); + + matrix.compose(offset, orientation, scale); + + mesh.setMatrixAt(i, matrix); + } + + scene.add(mesh); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const time = performance.now(); + + mesh.rotation.y = time * 0.00005; + + const delta = (time - lastTime) / 5000; + tmpQ.set(moveQ.x * delta, moveQ.y * delta, moveQ.z * delta, 1).normalize(); + tmpM.makeRotationFromQuaternion(tmpQ); + + for (let i = 0, il = instances; i < il; i++) { + mesh.getMatrixAt(i, currentM); + currentM.multiply(tmpM); + mesh.setMatrixAt(i, currentM); + } + + mesh.instanceMatrix.needsUpdate = true; + mesh.computeBoundingSphere(); + + lastTime = time; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_buffergeometry_lines.ts b/examples-testing/examples/webgl_buffergeometry_lines.ts new file mode 100644 index 000000000..1aaa5ca4a --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_lines.ts @@ -0,0 +1,118 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats, clock; + +let camera, scene, renderer; + +let line; + +const segments = 10000; +const r = 800; +let t = 0; + +init(); + +function init() { + container = document.getElementById('container'); + + // + + camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 1, 4000); + camera.position.z = 2750; + + scene = new THREE.Scene(); + + clock = new THREE.Clock(); + + const geometry = new THREE.BufferGeometry(); + const material = new THREE.LineBasicMaterial({ vertexColors: true }); + + const positions = []; + const colors = []; + + for (let i = 0; i < segments; i++) { + const x = Math.random() * r - r / 2; + const y = Math.random() * r - r / 2; + const z = Math.random() * r - r / 2; + + // positions + + positions.push(x, y, z); + + // colors + + colors.push(x / r + 0.5); + colors.push(y / r + 0.5); + colors.push(z / r + 0.5); + } + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + generateMorphTargets(geometry); + + geometry.computeBoundingSphere(); + + line = new THREE.Line(geometry, material); + scene.add(line); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + + container.appendChild(renderer.domElement); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const delta = clock.getDelta(); + const time = clock.getElapsedTime(); + + line.rotation.x = time * 0.25; + line.rotation.y = time * 0.5; + + t += delta * 0.5; + line.morphTargetInfluences[0] = Math.abs(Math.sin(t)); + + renderer.render(scene, camera); + + stats.update(); +} + +function generateMorphTargets(geometry) { + const data = []; + + for (let i = 0; i < segments; i++) { + const x = Math.random() * r - r / 2; + const y = Math.random() * r - r / 2; + const z = Math.random() * r - r / 2; + + data.push(x, y, z); + } + + const morphTarget = new THREE.Float32BufferAttribute(data, 3); + morphTarget.name = 'target1'; + + geometry.morphAttributes.position = [morphTarget]; +} diff --git a/examples-testing/examples/webgl_buffergeometry_lines_indexed.ts b/examples-testing/examples/webgl_buffergeometry_lines_indexed.ts new file mode 100644 index 000000000..58296087e --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_lines_indexed.ts @@ -0,0 +1,179 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; + +let camera, scene, renderer; + +let parent_node; + +init(); + +function init() { + container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 9000; + + scene = new THREE.Scene(); + + const geometry = new THREE.BufferGeometry(); + const material = new THREE.LineBasicMaterial({ vertexColors: true }); + + const indices = []; + const positions = []; + const colors = []; + + let next_positions_index = 0; + + // + + const iteration_count = 4; + const rangle = (60 * Math.PI) / 180.0; + + function add_vertex(v) { + positions.push(v.x, v.y, v.z); + colors.push(Math.random() * 0.5 + 0.5, Math.random() * 0.5 + 0.5, 1); + + return next_positions_index++; + } + + // simple Koch curve + + function snowflake_iteration(p0, p4, depth) { + if (--depth < 0) { + const i = next_positions_index - 1; // p0 already there + add_vertex(p4); + indices.push(i, i + 1); + + return; + } + + const v = p4.clone().sub(p0); + const v_tier = v.clone().multiplyScalar(1 / 3); + const p1 = p0.clone().add(v_tier); + + const angle = Math.atan2(v.y, v.x) + rangle; + const length = v_tier.length(); + const p2 = p1.clone(); + p2.x += Math.cos(angle) * length; + p2.y += Math.sin(angle) * length; + + const p3 = p0.clone().add(v_tier).add(v_tier); + + snowflake_iteration(p0, p1, depth); + snowflake_iteration(p1, p2, depth); + snowflake_iteration(p2, p3, depth); + snowflake_iteration(p3, p4, depth); + } + + function snowflake(points, loop, x_offset) { + for (let iteration = 0; iteration != iteration_count; iteration++) { + add_vertex(points[0]); + + for (let p_index = 0, p_count = points.length - 1; p_index != p_count; p_index++) { + snowflake_iteration(points[p_index], points[p_index + 1], iteration); + } + + if (loop) snowflake_iteration(points[points.length - 1], points[0], iteration); + + // translate input curve for next iteration + + for (let p_index = 0, p_count = points.length; p_index != p_count; p_index++) { + points[p_index].x += x_offset; + } + } + } + + let y = 0; + + snowflake([new THREE.Vector3(0, y, 0), new THREE.Vector3(500, y, 0)], false, 600); + + y += 600; + snowflake( + [new THREE.Vector3(0, y, 0), new THREE.Vector3(250, y + 400, 0), new THREE.Vector3(500, y, 0)], + true, + 600, + ); + + y += 600; + snowflake( + [ + new THREE.Vector3(0, y, 0), + new THREE.Vector3(500, y, 0), + new THREE.Vector3(500, y + 500, 0), + new THREE.Vector3(0, y + 500, 0), + ], + true, + 600, + ); + + y += 1000; + snowflake( + [ + new THREE.Vector3(250, y, 0), + new THREE.Vector3(500, y, 0), + new THREE.Vector3(250, y, 0), + new THREE.Vector3(250, y + 250, 0), + new THREE.Vector3(250, y, 0), + new THREE.Vector3(0, y, 0), + new THREE.Vector3(250, y, 0), + new THREE.Vector3(250, y - 250, 0), + new THREE.Vector3(250, y, 0), + ], + false, + 600, + ); + + // + + geometry.setIndex(indices); + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + geometry.computeBoundingSphere(); + + const lineSegments = new THREE.LineSegments(geometry, material); + lineSegments.position.x -= 1200; + lineSegments.position.y -= 1200; + + parent_node = new THREE.Object3D(); + parent_node.add(lineSegments); + + scene.add(parent_node); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + + container.appendChild(renderer.domElement); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const time = Date.now() * 0.001; + + parent_node.rotation.z = time * 0.5; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_buffergeometry_points.ts b/examples-testing/examples/webgl_buffergeometry_points.ts new file mode 100644 index 000000000..4547d9d08 --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_points.ts @@ -0,0 +1,109 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; + +let camera, scene, renderer; + +let points; + +init(); +animate(); + +function init() { + container = document.getElementById('container'); + + // + + camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 5, 3500); + camera.position.z = 2750; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050505); + scene.fog = new THREE.Fog(0x050505, 2000, 3500); + + // + + const particles = 500000; + + const geometry = new THREE.BufferGeometry(); + + const positions = []; + const colors = []; + + const color = new THREE.Color(); + + const n = 1000, + n2 = n / 2; // particles spread in the cube + + for (let i = 0; i < particles; i++) { + // positions + + const x = Math.random() * n - n2; + const y = Math.random() * n - n2; + const z = Math.random() * n - n2; + + positions.push(x, y, z); + + // colors + + const vx = x / n + 0.5; + const vy = y / n + 0.5; + const vz = z / n + 0.5; + + color.setRGB(vx, vy, vz, THREE.SRGBColorSpace); + + colors.push(color.r, color.g, color.b); + } + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + + geometry.computeBoundingSphere(); + + // + + const material = new THREE.PointsMaterial({ size: 15, vertexColors: true }); + + points = new THREE.Points(geometry, material); + scene.add(points); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + + container.appendChild(renderer.domElement); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const time = Date.now() * 0.001; + + points.rotation.x = time * 0.25; + points.rotation.y = time * 0.5; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_buffergeometry_points_interleaved.ts b/examples-testing/examples/webgl_buffergeometry_points_interleaved.ts new file mode 100644 index 000000000..93eed992e --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_points_interleaved.ts @@ -0,0 +1,122 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; + +let camera, scene, renderer; + +let points; + +init(); + +function init() { + container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 5, 3500); + camera.position.z = 2750; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050505); + scene.fog = new THREE.Fog(0x050505, 2000, 3500); + + // + + const particles = 500000; + + const geometry = new THREE.BufferGeometry(); + + // create a generic buffer of binary data (a single particle has 16 bytes of data) + + const arrayBuffer = new ArrayBuffer(particles * 16); + + // the following typed arrays share the same buffer + + const interleavedFloat32Buffer = new Float32Array(arrayBuffer); + const interleavedUint8Buffer = new Uint8Array(arrayBuffer); + + // + + const color = new THREE.Color(); + + const n = 1000, + n2 = n / 2; // particles spread in the cube + + for (let i = 0; i < interleavedFloat32Buffer.length; i += 4) { + // position (first 12 bytes) + + const x = Math.random() * n - n2; + const y = Math.random() * n - n2; + const z = Math.random() * n - n2; + + interleavedFloat32Buffer[i + 0] = x; + interleavedFloat32Buffer[i + 1] = y; + interleavedFloat32Buffer[i + 2] = z; + + // color (last 4 bytes) + + const vx = x / n + 0.5; + const vy = y / n + 0.5; + const vz = z / n + 0.5; + + color.setRGB(vx, vy, vz, THREE.SRGBColorSpace); + + const j = (i + 3) * 4; + + interleavedUint8Buffer[j + 0] = color.r * 255; + interleavedUint8Buffer[j + 1] = color.g * 255; + interleavedUint8Buffer[j + 2] = color.b * 255; + interleavedUint8Buffer[j + 3] = 0; // not needed + } + + const interleavedBuffer32 = new THREE.InterleavedBuffer(interleavedFloat32Buffer, 4); + const interleavedBuffer8 = new THREE.InterleavedBuffer(interleavedUint8Buffer, 16); + + geometry.setAttribute('position', new THREE.InterleavedBufferAttribute(interleavedBuffer32, 3, 0, false)); + geometry.setAttribute('color', new THREE.InterleavedBufferAttribute(interleavedBuffer8, 3, 12, true)); + + // + + const material = new THREE.PointsMaterial({ size: 15, vertexColors: true }); + + points = new THREE.Points(geometry, material); + scene.add(points); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + + container.appendChild(renderer.domElement); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const time = Date.now() * 0.001; + + points.rotation.x = time * 0.25; + points.rotation.y = time * 0.5; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_buffergeometry_rawshader.ts b/examples-testing/examples/webgl_buffergeometry_rawshader.ts new file mode 100644 index 000000000..5bc113dc3 --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_rawshader.ts @@ -0,0 +1,97 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; + +let camera, scene, renderer; + +init(); + +function init() { + container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10); + camera.position.z = 2; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x101010); + + // geometry + // nr of triangles with 3 vertices per triangle + const vertexCount = 200 * 3; + + const geometry = new THREE.BufferGeometry(); + + const positions = []; + const colors = []; + + for (let i = 0; i < vertexCount; i++) { + // adding x,y,z + positions.push(Math.random() - 0.5); + positions.push(Math.random() - 0.5); + positions.push(Math.random() - 0.5); + + // adding r,g,b,a + colors.push(Math.random() * 255); + colors.push(Math.random() * 255); + colors.push(Math.random() * 255); + colors.push(Math.random() * 255); + } + + const positionAttribute = new THREE.Float32BufferAttribute(positions, 3); + const colorAttribute = new THREE.Uint8BufferAttribute(colors, 4); + + colorAttribute.normalized = true; // this will map the buffer values to 0.0f - +1.0f in the shader + + geometry.setAttribute('position', positionAttribute); + geometry.setAttribute('color', colorAttribute); + + // material + + const material = new THREE.RawShaderMaterial({ + uniforms: { + time: { value: 1.0 }, + }, + vertexShader: document.getElementById('vertexShader').textContent, + fragmentShader: document.getElementById('fragmentShader').textContent, + side: THREE.DoubleSide, + transparent: true, + }); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const time = performance.now(); + + const object = scene.children[0]; + + object.rotation.y = time * 0.0005; + object.material.uniforms.time.value = time * 0.005; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_buffergeometry_selective_draw.ts b/examples-testing/examples/webgl_buffergeometry_selective_draw.ts new file mode 100644 index 000000000..d07176c51 --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_selective_draw.ts @@ -0,0 +1,150 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let camera, scene, renderer, stats; +let geometry, mesh; +const numLat = 100; +const numLng = 200; +let numLinesCulled = 0; + +init(); + +function init() { + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 10); + camera.position.z = 3.5; + + stats = new Stats(); + document.body.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); + + addLines(1.0); + + const hideLinesButton = document.getElementById('hideLines'); + hideLinesButton.addEventListener('click', hideLines); + + const showAllLinesButton = document.getElementById('showAllLines'); + showAllLinesButton.addEventListener('click', showAllLines); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); +} + +function addLines(radius) { + geometry = new THREE.BufferGeometry(); + const linePositions = new Float32Array(numLat * numLng * 3 * 2); + const lineColors = new Float32Array(numLat * numLng * 3 * 2); + const visible = new Float32Array(numLat * numLng * 2); + + for (let i = 0; i < numLat; ++i) { + for (let j = 0; j < numLng; ++j) { + const lat = (Math.random() * Math.PI) / 50.0 + (i / numLat) * Math.PI; + const lng = (Math.random() * Math.PI) / 50.0 + (j / numLng) * 2 * Math.PI; + + const index = i * numLng + j; + + linePositions[index * 6 + 0] = 0; + linePositions[index * 6 + 1] = 0; + linePositions[index * 6 + 2] = 0; + linePositions[index * 6 + 3] = radius * Math.sin(lat) * Math.cos(lng); + linePositions[index * 6 + 4] = radius * Math.cos(lat); + linePositions[index * 6 + 5] = radius * Math.sin(lat) * Math.sin(lng); + + const color = new THREE.Color(0xffffff); + + color.setHSL(lat / Math.PI, 1.0, 0.2); + lineColors[index * 6 + 0] = color.r; + lineColors[index * 6 + 1] = color.g; + lineColors[index * 6 + 2] = color.b; + + color.setHSL(lat / Math.PI, 1.0, 0.7); + lineColors[index * 6 + 3] = color.r; + lineColors[index * 6 + 4] = color.g; + lineColors[index * 6 + 5] = color.b; + + // non-0 is visible + visible[index * 2 + 0] = 1.0; + visible[index * 2 + 1] = 1.0; + } + } + + geometry.setAttribute('position', new THREE.BufferAttribute(linePositions, 3)); + geometry.setAttribute('vertColor', new THREE.BufferAttribute(lineColors, 3)); + geometry.setAttribute('visible', new THREE.BufferAttribute(visible, 1)); + + geometry.computeBoundingSphere(); + + const shaderMaterial = new THREE.ShaderMaterial({ + vertexShader: document.getElementById('vertexshader').textContent, + fragmentShader: document.getElementById('fragmentshader').textContent, + }); + + mesh = new THREE.LineSegments(geometry, shaderMaterial); + scene.add(mesh); + + updateCount(); +} + +function updateCount() { + const str = + '1 draw call, ' + + numLat * numLng + + ' lines, ' + + numLinesCulled + + ' culled (author)'; + document.getElementById('title').innerHTML = str.replace(/\B(?=(\d{3})+(?!\d))/g, ','); +} + +function hideLines() { + for (let i = 0; i < geometry.attributes.visible.array.length; i += 2) { + if (Math.random() > 0.75) { + if (geometry.attributes.visible.array[i + 0]) { + ++numLinesCulled; + } + + geometry.attributes.visible.array[i + 0] = 0; + geometry.attributes.visible.array[i + 1] = 0; + } + } + + geometry.attributes.visible.needsUpdate = true; + + updateCount(); +} + +function showAllLines() { + numLinesCulled = 0; + + for (let i = 0; i < geometry.attributes.visible.array.length; i += 2) { + geometry.attributes.visible.array[i + 0] = 1; + geometry.attributes.visible.array[i + 1] = 1; + } + + geometry.attributes.visible.needsUpdate = true; + + updateCount(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const time = Date.now() * 0.001; + + mesh.rotation.x = time * 0.25; + mesh.rotation.y = time * 0.5; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_buffergeometry_uint.ts b/examples-testing/examples/webgl_buffergeometry_uint.ts new file mode 100644 index 000000000..0b8df6ec7 --- /dev/null +++ b/examples-testing/examples/webgl_buffergeometry_uint.ts @@ -0,0 +1,177 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; + +let camera, scene, renderer; + +let mesh; + +init(); + +function init() { + container = document.getElementById('container'); + + // + + camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 1, 3500); + camera.position.z = 2750; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050505); + scene.fog = new THREE.Fog(0x050505, 2000, 3500); + + // + + scene.add(new THREE.AmbientLight(0xcccccc)); + + const light1 = new THREE.DirectionalLight(0xffffff, 1.5); + light1.position.set(1, 1, 1); + scene.add(light1); + + const light2 = new THREE.DirectionalLight(0xffffff, 4.5); + light2.position.set(0, -1, 0); + scene.add(light2); + + // + + const triangles = 500000; + + const geometry = new THREE.BufferGeometry(); + + const positions = []; + const normals = []; + const colors = []; + + const color = new THREE.Color(); + + const n = 800, + n2 = n / 2; // triangles spread in the cube + const d = 12, + d2 = d / 2; // individual triangle size + + const pA = new THREE.Vector3(); + const pB = new THREE.Vector3(); + const pC = new THREE.Vector3(); + + const cb = new THREE.Vector3(); + const ab = new THREE.Vector3(); + + for (let i = 0; i < triangles; i++) { + // positions + + const x = Math.random() * n - n2; + const y = Math.random() * n - n2; + const z = Math.random() * n - n2; + + const ax = x + Math.random() * d - d2; + const ay = y + Math.random() * d - d2; + const az = z + Math.random() * d - d2; + + const bx = x + Math.random() * d - d2; + const by = y + Math.random() * d - d2; + const bz = z + Math.random() * d - d2; + + const cx = x + Math.random() * d - d2; + const cy = y + Math.random() * d - d2; + const cz = z + Math.random() * d - d2; + + positions.push(ax, ay, az); + positions.push(bx, by, bz); + positions.push(cx, cy, cz); + + // flat face normals + + pA.set(ax, ay, az); + pB.set(bx, by, bz); + pC.set(cx, cy, cz); + + cb.subVectors(pC, pB); + ab.subVectors(pA, pB); + cb.cross(ab); + + cb.normalize(); + + const nx = cb.x; + const ny = cb.y; + const nz = cb.z; + + normals.push(nx * 32767, ny * 32767, nz * 32767); + normals.push(nx * 32767, ny * 32767, nz * 32767); + normals.push(nx * 32767, ny * 32767, nz * 32767); + + // colors + + const vx = x / n + 0.5; + const vy = y / n + 0.5; + const vz = z / n + 0.5; + + color.setRGB(vx, vy, vz); + + colors.push(color.r * 255, color.g * 255, color.b * 255); + colors.push(color.r * 255, color.g * 255, color.b * 255); + colors.push(color.r * 255, color.g * 255, color.b * 255); + } + + const positionAttribute = new THREE.Float32BufferAttribute(positions, 3); + const normalAttribute = new THREE.Int16BufferAttribute(normals, 3); + const colorAttribute = new THREE.Uint8BufferAttribute(colors, 3); + + normalAttribute.normalized = true; // this will map the buffer values to 0.0f - +1.0f in the shader + colorAttribute.normalized = true; + + geometry.setAttribute('position', positionAttribute); + geometry.setAttribute('normal', normalAttribute); + geometry.setAttribute('color', colorAttribute); + + geometry.computeBoundingSphere(); + + const material = new THREE.MeshPhongMaterial({ + color: 0xd5d5d5, + specular: 0xffffff, + shininess: 250, + side: THREE.DoubleSide, + vertexColors: true, + }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const time = Date.now() * 0.001; + + mesh.rotation.x = time * 0.25; + mesh.rotation.y = time * 0.5; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_camera.ts b/examples-testing/examples/webgl_camera.ts new file mode 100644 index 000000000..f3d663603 --- /dev/null +++ b/examples-testing/examples/webgl_camera.ts @@ -0,0 +1,218 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let SCREEN_WIDTH = window.innerWidth; +let SCREEN_HEIGHT = window.innerHeight; +let aspect = SCREEN_WIDTH / SCREEN_HEIGHT; + +let container, stats; +let camera, scene, renderer, mesh; +let cameraRig, activeCamera, activeHelper; +let cameraPerspective, cameraOrtho; +let cameraPerspectiveHelper, cameraOrthoHelper; +const frustumSize = 600; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + scene = new THREE.Scene(); + + // + + camera = new THREE.PerspectiveCamera(50, 0.5 * aspect, 1, 10000); + camera.position.z = 2500; + + cameraPerspective = new THREE.PerspectiveCamera(50, 0.5 * aspect, 150, 1000); + + cameraPerspectiveHelper = new THREE.CameraHelper(cameraPerspective); + scene.add(cameraPerspectiveHelper); + + // + cameraOrtho = new THREE.OrthographicCamera( + (0.5 * frustumSize * aspect) / -2, + (0.5 * frustumSize * aspect) / 2, + frustumSize / 2, + frustumSize / -2, + 150, + 1000, + ); + + cameraOrthoHelper = new THREE.CameraHelper(cameraOrtho); + scene.add(cameraOrthoHelper); + + // + + activeCamera = cameraPerspective; + activeHelper = cameraPerspectiveHelper; + + // counteract different front orientation of cameras vs rig + + cameraOrtho.rotation.y = Math.PI; + cameraPerspective.rotation.y = Math.PI; + + cameraRig = new THREE.Group(); + + cameraRig.add(cameraPerspective); + cameraRig.add(cameraOrtho); + + scene.add(cameraRig); + + // + + mesh = new THREE.Mesh( + new THREE.SphereGeometry(100, 16, 8), + new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true }), + ); + scene.add(mesh); + + const mesh2 = new THREE.Mesh( + new THREE.SphereGeometry(50, 16, 8), + new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true }), + ); + mesh2.position.y = 150; + mesh.add(mesh2); + + const mesh3 = new THREE.Mesh( + new THREE.SphereGeometry(5, 16, 8), + new THREE.MeshBasicMaterial({ color: 0x0000ff, wireframe: true }), + ); + mesh3.position.z = 150; + cameraRig.add(mesh3); + + // + + const geometry = new THREE.BufferGeometry(); + const vertices = []; + + for (let i = 0; i < 10000; i++) { + vertices.push(THREE.MathUtils.randFloatSpread(2000)); // x + vertices.push(THREE.MathUtils.randFloatSpread(2000)); // y + vertices.push(THREE.MathUtils.randFloatSpread(2000)); // z + } + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + + const particles = new THREE.Points(geometry, new THREE.PointsMaterial({ color: 0x888888 })); + scene.add(particles); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + renderer.setScissorTest(true); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); + document.addEventListener('keydown', onKeyDown); +} + +// + +function onKeyDown(event) { + switch (event.keyCode) { + case 79 /*O*/: + activeCamera = cameraOrtho; + activeHelper = cameraOrthoHelper; + + break; + + case 80 /*P*/: + activeCamera = cameraPerspective; + activeHelper = cameraPerspectiveHelper; + + break; + } +} + +// + +function onWindowResize() { + SCREEN_WIDTH = window.innerWidth; + SCREEN_HEIGHT = window.innerHeight; + aspect = SCREEN_WIDTH / SCREEN_HEIGHT; + + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + + camera.aspect = 0.5 * aspect; + camera.updateProjectionMatrix(); + + cameraPerspective.aspect = 0.5 * aspect; + cameraPerspective.updateProjectionMatrix(); + + cameraOrtho.left = (-0.5 * frustumSize * aspect) / 2; + cameraOrtho.right = (0.5 * frustumSize * aspect) / 2; + cameraOrtho.top = frustumSize / 2; + cameraOrtho.bottom = -frustumSize / 2; + cameraOrtho.updateProjectionMatrix(); +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + const r = Date.now() * 0.0005; + + mesh.position.x = 700 * Math.cos(r); + mesh.position.z = 700 * Math.sin(r); + mesh.position.y = 700 * Math.sin(r); + + mesh.children[0].position.x = 70 * Math.cos(2 * r); + mesh.children[0].position.z = 70 * Math.sin(r); + + if (activeCamera === cameraPerspective) { + cameraPerspective.fov = 35 + 30 * Math.sin(0.5 * r); + cameraPerspective.far = mesh.position.length(); + cameraPerspective.updateProjectionMatrix(); + + cameraPerspectiveHelper.update(); + cameraPerspectiveHelper.visible = true; + + cameraOrthoHelper.visible = false; + } else { + cameraOrtho.far = mesh.position.length(); + cameraOrtho.updateProjectionMatrix(); + + cameraOrthoHelper.update(); + cameraOrthoHelper.visible = true; + + cameraPerspectiveHelper.visible = false; + } + + cameraRig.lookAt(mesh.position); + + // + + activeHelper.visible = false; + + renderer.setClearColor(0x000000, 1); + renderer.setScissor(0, 0, SCREEN_WIDTH / 2, SCREEN_HEIGHT); + renderer.setViewport(0, 0, SCREEN_WIDTH / 2, SCREEN_HEIGHT); + renderer.render(scene, activeCamera); + + // + + activeHelper.visible = true; + + renderer.setClearColor(0x111111, 1); + renderer.setScissor(SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2, SCREEN_HEIGHT); + renderer.setViewport(SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2, SCREEN_HEIGHT); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_camera_array.ts b/examples-testing/examples/webgl_camera_array.ts new file mode 100644 index 000000000..8b10e27cb --- /dev/null +++ b/examples-testing/examples/webgl_camera_array.ts @@ -0,0 +1,104 @@ +import * as THREE from 'three'; + +let camera, scene, renderer; +let mesh; +const AMOUNT = 6; + +init(); + +function init() { + const ASPECT_RATIO = window.innerWidth / window.innerHeight; + + const WIDTH = (window.innerWidth / AMOUNT) * window.devicePixelRatio; + const HEIGHT = (window.innerHeight / AMOUNT) * window.devicePixelRatio; + + const cameras = []; + + for (let y = 0; y < AMOUNT; y++) { + for (let x = 0; x < AMOUNT; x++) { + const subcamera = new THREE.PerspectiveCamera(40, ASPECT_RATIO, 0.1, 10); + subcamera.viewport = new THREE.Vector4( + Math.floor(x * WIDTH), + Math.floor(y * HEIGHT), + Math.ceil(WIDTH), + Math.ceil(HEIGHT), + ); + subcamera.position.x = x / AMOUNT - 0.5; + subcamera.position.y = 0.5 - y / AMOUNT; + subcamera.position.z = 1.5; + subcamera.position.multiplyScalar(2); + subcamera.lookAt(0, 0, 0); + subcamera.updateMatrixWorld(); + cameras.push(subcamera); + } + } + + camera = new THREE.ArrayCamera(cameras); + camera.position.z = 3; + + scene = new THREE.Scene(); + + scene.add(new THREE.AmbientLight(0x999999)); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0.5, 0.5, 1); + light.castShadow = true; + light.shadow.camera.zoom = 4; // tighter shadow map + scene.add(light); + + const geometryBackground = new THREE.PlaneGeometry(100, 100); + const materialBackground = new THREE.MeshPhongMaterial({ color: 0x000066 }); + + const background = new THREE.Mesh(geometryBackground, materialBackground); + background.receiveShadow = true; + background.position.set(0, 0, -1); + scene.add(background); + + const geometryCylinder = new THREE.CylinderGeometry(0.5, 0.5, 1, 32); + const materialCylinder = new THREE.MeshPhongMaterial({ color: 0xff0000 }); + + mesh = new THREE.Mesh(geometryCylinder, materialCylinder); + mesh.castShadow = true; + mesh.receiveShadow = true; + scene.add(mesh); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + document.body.appendChild(renderer.domElement); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const ASPECT_RATIO = window.innerWidth / window.innerHeight; + const WIDTH = (window.innerWidth / AMOUNT) * window.devicePixelRatio; + const HEIGHT = (window.innerHeight / AMOUNT) * window.devicePixelRatio; + + camera.aspect = ASPECT_RATIO; + camera.updateProjectionMatrix(); + + for (let y = 0; y < AMOUNT; y++) { + for (let x = 0; x < AMOUNT; x++) { + const subcamera = camera.cameras[AMOUNT * y + x]; + + subcamera.viewport.set(Math.floor(x * WIDTH), Math.floor(y * HEIGHT), Math.ceil(WIDTH), Math.ceil(HEIGHT)); + + subcamera.aspect = ASPECT_RATIO; + subcamera.updateProjectionMatrix(); + } + } + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + mesh.rotation.x += 0.005; + mesh.rotation.z += 0.01; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_camera_logarithmicdepthbuffer.ts b/examples-testing/examples/webgl_camera_logarithmicdepthbuffer.ts new file mode 100644 index 000000000..f1d440004 --- /dev/null +++ b/examples-testing/examples/webgl_camera_logarithmicdepthbuffer.ts @@ -0,0 +1,248 @@ +import * as THREE from 'three'; + +import { FontLoader } from 'three/addons/loaders/FontLoader.js'; +import { TextGeometry } from 'three/addons/geometries/TextGeometry.js'; + +import Stats from 'three/addons/libs/stats.module.js'; + +// 1 micrometer to 100 billion light years in one scene, with 1 unit = 1 meter? preposterous! and yet... +const NEAR = 1e-6, + FAR = 1e27; +let SCREEN_WIDTH = window.innerWidth; +let SCREEN_HEIGHT = window.innerHeight; +let screensplit = 0.25, + screensplit_right = 0; +const mouse = [0.5, 0.5]; +let zoompos = -100, + minzoomspeed = 0.015; +let zoomspeed = minzoomspeed; + +let container, border, stats; +const objects = {}; + +// Generate a number of text labels, from 1µm in size up to 100,000,000 light years +// Try to use some descriptive real-world examples of objects at each scale + +const labeldata = [ + { size: 0.01, scale: 0.0001, label: 'microscopic (1µm)' }, // FIXME - triangulating text fails at this size, so we scale instead + { size: 0.01, scale: 0.1, label: 'minuscule (1mm)' }, + { size: 0.01, scale: 1.0, label: 'tiny (1cm)' }, + { size: 1, scale: 1.0, label: 'child-sized (1m)' }, + { size: 10, scale: 1.0, label: 'tree-sized (10m)' }, + { size: 100, scale: 1.0, label: 'building-sized (100m)' }, + { size: 1000, scale: 1.0, label: 'medium (1km)' }, + { size: 10000, scale: 1.0, label: 'city-sized (10km)' }, + { size: 3400000, scale: 1.0, label: 'moon-sized (3,400 Km)' }, + { size: 12000000, scale: 1.0, label: 'planet-sized (12,000 km)' }, + { size: 1400000000, scale: 1.0, label: 'sun-sized (1,400,000 km)' }, + { size: 7.47e12, scale: 1.0, label: 'solar system-sized (50Au)' }, + { size: 9.4605284e15, scale: 1.0, label: 'gargantuan (1 light year)' }, + { size: 3.08567758e16, scale: 1.0, label: 'ludicrous (1 parsec)' }, + { size: 1e19, scale: 1.0, label: 'mind boggling (1000 light years)' }, +]; + +init(); + +function init() { + container = document.getElementById('container'); + + const loader = new FontLoader(); + loader.load('fonts/helvetiker_regular.typeface.json', function (font) { + const scene = initScene(font); + + // Initialize two copies of the same scene, one with normal z-buffer and one with logarithmic z-buffer + objects.normal = initView(scene, 'normal', false); + objects.logzbuf = initView(scene, 'logzbuf', true); + + animate(); + }); + + stats = new Stats(); + container.appendChild(stats.dom); + + // Resize border allows the user to easily compare effects of logarithmic depth buffer over the whole scene + border = document.getElementById('renderer_border'); + border.addEventListener('pointerdown', onBorderPointerDown); + + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('resize', onWindowResize); + window.addEventListener('wheel', onMouseWheel); +} + +function initView(scene, name, logDepthBuf) { + const framecontainer = document.getElementById('container_' + name); + + const camera = new THREE.PerspectiveCamera(50, (screensplit * SCREEN_WIDTH) / SCREEN_HEIGHT, NEAR, FAR); + scene.add(camera); + + const renderer = new THREE.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: logDepthBuf }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(SCREEN_WIDTH / 2, SCREEN_HEIGHT); + renderer.domElement.style.position = 'relative'; + renderer.domElement.id = 'renderer_' + name; + framecontainer.appendChild(renderer.domElement); + + return { container: framecontainer, renderer: renderer, scene: scene, camera: camera }; +} + +function initScene(font) { + const scene = new THREE.Scene(); + + scene.add(new THREE.AmbientLight(0x777777)); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(100, 100, 100); + scene.add(light); + + const materialargs = { + color: 0xffffff, + specular: 0x050505, + shininess: 50, + emissive: 0x000000, + }; + + const geometry = new THREE.SphereGeometry(0.5, 24, 12); + + for (let i = 0; i < labeldata.length; i++) { + const scale = labeldata[i].scale || 1; + + const labelgeo = new TextGeometry(labeldata[i].label, { + font: font, + size: labeldata[i].size, + depth: labeldata[i].size / 2, + }); + + labelgeo.computeBoundingSphere(); + + // center text + labelgeo.translate(-labelgeo.boundingSphere.radius, 0, 0); + + materialargs.color = new THREE.Color().setHSL(Math.random(), 0.5, 0.5); + + const material = new THREE.MeshPhongMaterial(materialargs); + + const group = new THREE.Group(); + group.position.z = -labeldata[i].size * scale; + scene.add(group); + + const textmesh = new THREE.Mesh(labelgeo, material); + textmesh.scale.set(scale, scale, scale); + textmesh.position.z = -labeldata[i].size * scale; + textmesh.position.y = (labeldata[i].size / 4) * scale; + group.add(textmesh); + + const dotmesh = new THREE.Mesh(geometry, material); + dotmesh.position.y = (-labeldata[i].size / 4) * scale; + dotmesh.scale.multiplyScalar(labeldata[i].size * scale); + group.add(dotmesh); + } + + return scene; +} + +function updateRendererSizes() { + // Recalculate size for both renderers when screen size or split location changes + + SCREEN_WIDTH = window.innerWidth; + SCREEN_HEIGHT = window.innerHeight; + + screensplit_right = 1 - screensplit; + + objects.normal.renderer.setSize(screensplit * SCREEN_WIDTH, SCREEN_HEIGHT); + objects.normal.camera.aspect = (screensplit * SCREEN_WIDTH) / SCREEN_HEIGHT; + objects.normal.camera.updateProjectionMatrix(); + objects.normal.camera.setViewOffset(SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, SCREEN_WIDTH * screensplit, SCREEN_HEIGHT); + objects.normal.container.style.width = screensplit * 100 + '%'; + + objects.logzbuf.renderer.setSize(screensplit_right * SCREEN_WIDTH, SCREEN_HEIGHT); + objects.logzbuf.camera.aspect = (screensplit_right * SCREEN_WIDTH) / SCREEN_HEIGHT; + objects.logzbuf.camera.updateProjectionMatrix(); + objects.logzbuf.camera.setViewOffset( + SCREEN_WIDTH, + SCREEN_HEIGHT, + SCREEN_WIDTH * screensplit, + 0, + SCREEN_WIDTH * screensplit_right, + SCREEN_HEIGHT, + ); + objects.logzbuf.container.style.width = screensplit_right * 100 + '%'; + + border.style.left = screensplit * 100 + '%'; +} + +function animate() { + requestAnimationFrame(animate); + render(); +} + +function render() { + // Put some limits on zooming + const minzoom = labeldata[0].size * labeldata[0].scale * 1; + const maxzoom = labeldata[labeldata.length - 1].size * labeldata[labeldata.length - 1].scale * 100; + let damping = Math.abs(zoomspeed) > minzoomspeed ? 0.95 : 1.0; + + // Zoom out faster the further out you go + const zoom = THREE.MathUtils.clamp(Math.pow(Math.E, zoompos), minzoom, maxzoom); + zoompos = Math.log(zoom); + + // Slow down quickly at the zoom limits + if ((zoom == minzoom && zoomspeed < 0) || (zoom == maxzoom && zoomspeed > 0)) { + damping = 0.85; + } + + zoompos += zoomspeed; + zoomspeed *= damping; + + objects.normal.camera.position.x = Math.sin(0.5 * Math.PI * (mouse[0] - 0.5)) * zoom; + objects.normal.camera.position.y = Math.sin(0.25 * Math.PI * (mouse[1] - 0.5)) * zoom; + objects.normal.camera.position.z = Math.cos(0.5 * Math.PI * (mouse[0] - 0.5)) * zoom; + objects.normal.camera.lookAt(objects.normal.scene.position); + + // Clone camera settings across both scenes + objects.logzbuf.camera.position.copy(objects.normal.camera.position); + objects.logzbuf.camera.quaternion.copy(objects.normal.camera.quaternion); + + // Update renderer sizes if the split has changed + if (screensplit_right != 1 - screensplit) { + updateRendererSizes(); + } + + objects.normal.renderer.render(objects.normal.scene, objects.normal.camera); + objects.logzbuf.renderer.render(objects.logzbuf.scene, objects.logzbuf.camera); + + stats.update(); +} + +function onWindowResize() { + updateRendererSizes(); +} + +function onBorderPointerDown() { + // activate draggable window resizing bar + window.addEventListener('pointermove', onBorderPointerMove); + window.addEventListener('pointerup', onBorderPointerUp); +} + +function onBorderPointerMove(ev) { + screensplit = Math.max(0, Math.min(1, ev.clientX / window.innerWidth)); +} + +function onBorderPointerUp() { + window.removeEventListener('pointermove', onBorderPointerMove); + window.removeEventListener('pointerup', onBorderPointerUp); +} + +function onMouseMove(ev) { + mouse[0] = ev.clientX / window.innerWidth; + mouse[1] = ev.clientY / window.innerHeight; +} + +function onMouseWheel(ev) { + const amount = ev.deltaY; + if (amount === 0) return; + const dir = amount / Math.abs(amount); + zoomspeed = dir / 10; + + // Slow down default zoom speed after user starts zooming, to give them more control + minzoomspeed = 0.001; +} diff --git a/examples-testing/examples/webgl_clipculldistance.ts b/examples-testing/examples/webgl_clipculldistance.ts new file mode 100644 index 000000000..a5fb54d47 --- /dev/null +++ b/examples-testing/examples/webgl_clipculldistance.ts @@ -0,0 +1,110 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import Stats from 'three/addons/libs/stats.module.js'; + +let camera, controls, clock, scene, renderer, stats; + +let material; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10); + camera.position.z = 2; + + scene = new THREE.Scene(); + + clock = new THREE.Clock(); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + if (renderer.extensions.has('WEBGL_clip_cull_distance') === false) { + document.getElementById('notSupported').style.display = ''; + return; + } + + const ext = renderer.getContext().getExtension('WEBGL_clip_cull_distance'); + const gl = renderer.getContext(); + + gl.enable(ext.CLIP_DISTANCE0_WEBGL); + + // geometry + + const vertexCount = 200 * 3; + + const geometry = new THREE.BufferGeometry(); + + const positions = []; + const colors = []; + + for (let i = 0; i < vertexCount; i++) { + // adding x,y,z + positions.push(Math.random() - 0.5); + positions.push(Math.random() - 0.5); + positions.push(Math.random() - 0.5); + + // adding r,g,b,a + colors.push(Math.random() * 255); + colors.push(Math.random() * 255); + colors.push(Math.random() * 255); + colors.push(Math.random() * 255); + } + + const positionAttribute = new THREE.Float32BufferAttribute(positions, 3); + const colorAttribute = new THREE.Uint8BufferAttribute(colors, 4); + colorAttribute.normalized = true; + + geometry.setAttribute('position', positionAttribute); + geometry.setAttribute('color', colorAttribute); + + // material + + material = new THREE.ShaderMaterial({ + uniforms: { + time: { value: 1.0 }, + }, + vertexShader: document.getElementById('vertexShader').textContent, + fragmentShader: document.getElementById('fragmentShader').textContent, + side: THREE.DoubleSide, + transparent: true, + vertexColors: true, + }); + + material.extensions.clipCullDistance = true; + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + controls = new OrbitControls(camera, renderer.domElement); + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(); + stats.update(); + + material.uniforms.time.value = clock.getElapsedTime(); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_clipping.ts b/examples-testing/examples/webgl_clipping.ts new file mode 100644 index 000000000..cde10c7d1 --- /dev/null +++ b/examples-testing/examples/webgl_clipping.ts @@ -0,0 +1,195 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer, startTime, object, stats; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(36, window.innerWidth / window.innerHeight, 0.25, 16); + + camera.position.set(0, 1.3, 3); + + scene = new THREE.Scene(); + + // Lights + + scene.add(new THREE.AmbientLight(0xcccccc)); + + const spotLight = new THREE.SpotLight(0xffffff, 60); + spotLight.angle = Math.PI / 5; + spotLight.penumbra = 0.2; + spotLight.position.set(2, 3, 3); + spotLight.castShadow = true; + spotLight.shadow.camera.near = 3; + spotLight.shadow.camera.far = 10; + spotLight.shadow.mapSize.width = 1024; + spotLight.shadow.mapSize.height = 1024; + scene.add(spotLight); + + const dirLight = new THREE.DirectionalLight(0x55505a, 3); + dirLight.position.set(0, 3, 0); + dirLight.castShadow = true; + dirLight.shadow.camera.near = 1; + dirLight.shadow.camera.far = 10; + + dirLight.shadow.camera.right = 1; + dirLight.shadow.camera.left = -1; + dirLight.shadow.camera.top = 1; + dirLight.shadow.camera.bottom = -1; + + dirLight.shadow.mapSize.width = 1024; + dirLight.shadow.mapSize.height = 1024; + scene.add(dirLight); + + // ***** Clipping planes: ***** + + const localPlane = new THREE.Plane(new THREE.Vector3(0, -1, 0), 0.8); + const globalPlane = new THREE.Plane(new THREE.Vector3(-1, 0, 0), 0.1); + + // Geometry + + const material = new THREE.MeshPhongMaterial({ + color: 0x80ee10, + shininess: 100, + side: THREE.DoubleSide, + + // ***** Clipping setup (material): ***** + clippingPlanes: [localPlane], + clipShadows: true, + + alphaToCoverage: true, + }); + + const geometry = new THREE.TorusKnotGeometry(0.4, 0.08, 95, 20); + + object = new THREE.Mesh(geometry, material); + object.castShadow = true; + scene.add(object); + + const ground = new THREE.Mesh( + new THREE.PlaneGeometry(9, 9, 1, 1), + new THREE.MeshPhongMaterial({ color: 0xa0adaf, shininess: 150 }), + ); + + ground.rotation.x = -Math.PI / 2; // rotates X/Y to X/Z + ground.receiveShadow = true; + scene.add(ground); + + // Renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + document.body.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); + + // ***** Clipping setup (renderer): ***** + const globalPlanes = [globalPlane], + Empty = Object.freeze([]); + renderer.clippingPlanes = Empty; // GUI sets it to globalPlanes + renderer.localClippingEnabled = true; + + // Stats + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // Controls + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 1, 0); + controls.update(); + + // GUI + + const gui = new GUI(), + props = { + alphaToCoverage: true, + }, + folderLocal = gui.addFolder('Local Clipping'), + propsLocal = { + get Enabled() { + return renderer.localClippingEnabled; + }, + set Enabled(v) { + renderer.localClippingEnabled = v; + }, + + get Shadows() { + return material.clipShadows; + }, + set Shadows(v) { + material.clipShadows = v; + }, + + get Plane() { + return localPlane.constant; + }, + set Plane(v) { + localPlane.constant = v; + }, + }, + folderGlobal = gui.addFolder('Global Clipping'), + propsGlobal = { + get Enabled() { + return renderer.clippingPlanes !== Empty; + }, + set Enabled(v) { + renderer.clippingPlanes = v ? globalPlanes : Empty; + }, + + get Plane() { + return globalPlane.constant; + }, + set Plane(v) { + globalPlane.constant = v; + }, + }; + + gui.add(props, 'alphaToCoverage').onChange(function (value) { + ground.material.alphaToCoverage = value; + ground.material.needsUpdate = true; + + material.alphaToCoverage = value; + material.needsUpdate = true; + }); + folderLocal.add(propsLocal, 'Enabled'); + folderLocal.add(propsLocal, 'Shadows'); + folderLocal.add(propsLocal, 'Plane', 0.3, 1.25); + + folderGlobal.add(propsGlobal, 'Enabled'); + folderGlobal.add(propsGlobal, 'Plane', -0.4, 3); + + // Start + + startTime = Date.now(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const currentTime = Date.now(); + const time = (currentTime - startTime) / 1000; + + object.position.y = 0.8; + object.rotation.x = time * 0.5; + object.rotation.y = time * 0.2; + object.scale.setScalar(Math.cos(time) * 0.125 + 0.875); + + stats.begin(); + renderer.render(scene, camera); + stats.end(); +} diff --git a/examples-testing/examples/webgl_clipping_advanced.ts b/examples-testing/examples/webgl_clipping_advanced.ts new file mode 100644 index 000000000..614d710da --- /dev/null +++ b/examples-testing/examples/webgl_clipping_advanced.ts @@ -0,0 +1,355 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +function planesFromMesh(vertices, indices) { + // creates a clipping volume from a convex triangular mesh + // specified by the arrays 'vertices' and 'indices' + + const n = indices.length / 3, + result = new Array(n); + + for (let i = 0, j = 0; i < n; ++i, j += 3) { + const a = vertices[indices[j]], + b = vertices[indices[j + 1]], + c = vertices[indices[j + 2]]; + + result[i] = new THREE.Plane().setFromCoplanarPoints(a, b, c); + } + + return result; +} + +function createPlanes(n) { + // creates an array of n uninitialized plane objects + + const result = new Array(n); + + for (let i = 0; i !== n; ++i) result[i] = new THREE.Plane(); + + return result; +} + +function assignTransformedPlanes(planesOut, planesIn, matrix) { + // sets an array of existing planes to transformed 'planesIn' + + for (let i = 0, n = planesIn.length; i !== n; ++i) planesOut[i].copy(planesIn[i]).applyMatrix4(matrix); +} + +function cylindricalPlanes(n, innerRadius) { + const result = createPlanes(n); + + for (let i = 0; i !== n; ++i) { + const plane = result[i], + angle = (i * Math.PI * 2) / n; + + plane.normal.set(Math.cos(angle), 0, Math.sin(angle)); + + plane.constant = innerRadius; + } + + return result; +} + +const planeToMatrix = (function () { + // creates a matrix that aligns X/Y to a given plane + + // temporaries: + const xAxis = new THREE.Vector3(), + yAxis = new THREE.Vector3(), + trans = new THREE.Vector3(); + + return function planeToMatrix(plane) { + const zAxis = plane.normal, + matrix = new THREE.Matrix4(); + + // Hughes & Moeller '99 + // "Building an Orthonormal Basis from a Unit Vector." + + if (Math.abs(zAxis.x) > Math.abs(zAxis.z)) { + yAxis.set(-zAxis.y, zAxis.x, 0); + } else { + yAxis.set(0, -zAxis.z, zAxis.y); + } + + xAxis.crossVectors(yAxis.normalize(), zAxis); + + plane.coplanarPoint(trans); + return matrix.set( + xAxis.x, + yAxis.x, + zAxis.x, + trans.x, + xAxis.y, + yAxis.y, + zAxis.y, + trans.y, + xAxis.z, + yAxis.z, + zAxis.z, + trans.z, + 0, + 0, + 0, + 1, + ); + }; +})(); + +// A regular tetrahedron for the clipping volume: + +const Vertices = [ + new THREE.Vector3(+1, 0, +Math.SQRT1_2), + new THREE.Vector3(-1, 0, +Math.SQRT1_2), + new THREE.Vector3(0, +1, -Math.SQRT1_2), + new THREE.Vector3(0, -1, -Math.SQRT1_2), + ], + Indices = [0, 1, 2, 0, 2, 3, 0, 3, 1, 1, 3, 2], + Planes = planesFromMesh(Vertices, Indices), + PlaneMatrices = Planes.map(planeToMatrix), + GlobalClippingPlanes = cylindricalPlanes(5, 2.5), + Empty = Object.freeze([]); + +let camera, scene, renderer, startTime, stats, object, clipMaterial, volumeVisualization, globalClippingPlanes; + +function init() { + camera = new THREE.PerspectiveCamera(36, window.innerWidth / window.innerHeight, 0.25, 16); + + camera.position.set(0, 1.5, 3); + + scene = new THREE.Scene(); + + // Lights + + scene.add(new THREE.AmbientLight(0xffffff)); + + const spotLight = new THREE.SpotLight(0xffffff, 60); + spotLight.angle = Math.PI / 5; + spotLight.penumbra = 0.2; + spotLight.position.set(2, 3, 3); + spotLight.castShadow = true; + spotLight.shadow.camera.near = 3; + spotLight.shadow.camera.far = 10; + spotLight.shadow.mapSize.width = 1024; + spotLight.shadow.mapSize.height = 1024; + scene.add(spotLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 1.5); + dirLight.position.set(0, 2, 0); + dirLight.castShadow = true; + dirLight.shadow.camera.near = 1; + dirLight.shadow.camera.far = 10; + + dirLight.shadow.camera.right = 1; + dirLight.shadow.camera.left = -1; + dirLight.shadow.camera.top = 1; + dirLight.shadow.camera.bottom = -1; + + dirLight.shadow.mapSize.width = 1024; + dirLight.shadow.mapSize.height = 1024; + scene.add(dirLight); + + // Geometry + + clipMaterial = new THREE.MeshPhongMaterial({ + color: 0xee0a10, + shininess: 100, + side: THREE.DoubleSide, + // Clipping setup: + clippingPlanes: createPlanes(Planes.length), + clipShadows: true, + }); + + object = new THREE.Group(); + + const geometry = new THREE.BoxGeometry(0.18, 0.18, 0.18); + + for (let z = -2; z <= 2; ++z) + for (let y = -2; y <= 2; ++y) + for (let x = -2; x <= 2; ++x) { + const mesh = new THREE.Mesh(geometry, clipMaterial); + mesh.position.set(x / 5, y / 5, z / 5); + mesh.castShadow = true; + object.add(mesh); + } + + scene.add(object); + + const planeGeometry = new THREE.PlaneGeometry(3, 3, 1, 1), + color = new THREE.Color(); + + volumeVisualization = new THREE.Group(); + volumeVisualization.visible = false; + + for (let i = 0, n = Planes.length; i !== n; ++i) { + const material = new THREE.MeshBasicMaterial({ + color: color.setHSL(i / n, 0.5, 0.5).getHex(), + side: THREE.DoubleSide, + + opacity: 0.2, + transparent: true, + + // clip to the others to show the volume (wildly + // intersecting transparent planes look bad) + clippingPlanes: clipMaterial.clippingPlanes.filter(function (_, j) { + return j !== i; + }), + + // no need to enable shadow clipping - the plane + // visualization does not cast shadows + }); + + const mesh = new THREE.Mesh(planeGeometry, material); + mesh.matrixAutoUpdate = false; + + volumeVisualization.add(mesh); + } + + scene.add(volumeVisualization); + + const ground = new THREE.Mesh( + planeGeometry, + new THREE.MeshPhongMaterial({ + color: 0xa0adaf, + shininess: 10, + }), + ); + ground.rotation.x = -Math.PI / 2; + ground.scale.multiplyScalar(3); + ground.receiveShadow = true; + scene.add(ground); + + // Renderer + + const container = document.body; + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + container.appendChild(renderer.domElement); + // Clipping setup: + globalClippingPlanes = createPlanes(GlobalClippingPlanes.length); + renderer.clippingPlanes = Empty; + renderer.localClippingEnabled = true; + + window.addEventListener('resize', onWindowResize); + + // Stats + + stats = new Stats(); + container.appendChild(stats.dom); + + // Controls + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 1; + controls.maxDistance = 8; + controls.target.set(0, 1, 0); + controls.update(); + + // GUI + + const gui = new GUI(), + folder = gui.addFolder('Local Clipping'), + props = { + get Enabled() { + return renderer.localClippingEnabled; + }, + set Enabled(v) { + renderer.localClippingEnabled = v; + if (!v) volumeVisualization.visible = false; + }, + + get Shadows() { + return clipMaterial.clipShadows; + }, + set Shadows(v) { + clipMaterial.clipShadows = v; + }, + + get Visualize() { + return volumeVisualization.visible; + }, + set Visualize(v) { + if (renderer.localClippingEnabled) volumeVisualization.visible = v; + }, + }; + + folder.add(props, 'Enabled'); + folder.add(props, 'Shadows'); + folder.add(props, 'Visualize').listen(); + + gui.addFolder('Global Clipping').add( + { + get Enabled() { + return renderer.clippingPlanes !== Empty; + }, + set Enabled(v) { + renderer.clippingPlanes = v ? globalClippingPlanes : Empty; + }, + }, + 'Enabled', + ); + + // Start + + startTime = Date.now(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function setObjectWorldMatrix(object, matrix) { + // set the orientation of an object based on a world matrix + + const parent = object.parent; + scene.updateMatrixWorld(); + object.matrix.copy(parent.matrixWorld).invert(); + object.applyMatrix4(matrix); +} + +const transform = new THREE.Matrix4(), + tmpMatrix = new THREE.Matrix4(); + +function animate() { + const currentTime = Date.now(), + time = (currentTime - startTime) / 1000; + + object.position.y = 1; + object.rotation.x = time * 0.5; + object.rotation.y = time * 0.2; + + object.updateMatrix(); + transform.copy(object.matrix); + + const bouncy = Math.cos(time * 0.5) * 0.5 + 0.7; + transform.multiply(tmpMatrix.makeScale(bouncy, bouncy, bouncy)); + + assignTransformedPlanes(clipMaterial.clippingPlanes, Planes, transform); + + const planeMeshes = volumeVisualization.children; + + for (let i = 0, n = planeMeshes.length; i !== n; ++i) { + tmpMatrix.multiplyMatrices(transform, PlaneMatrices[i]); + setObjectWorldMatrix(planeMeshes[i], tmpMatrix); + } + + transform.makeRotationY(time * 0.1); + + assignTransformedPlanes(globalClippingPlanes, GlobalClippingPlanes, transform); + + stats.begin(); + renderer.render(scene, camera); + stats.end(); +} + +init(); diff --git a/examples-testing/examples/webgl_clipping_intersection.ts b/examples-testing/examples/webgl_clipping_intersection.ts new file mode 100644 index 000000000..5f45e45df --- /dev/null +++ b/examples-testing/examples/webgl_clipping_intersection.ts @@ -0,0 +1,137 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer; + +const params = { + clipIntersection: true, + planeConstant: 0, + showHelpers: false, + alphaToCoverage: true, +}; + +const clipPlanes = [ + new THREE.Plane(new THREE.Vector3(1, 0, 0), 0), + new THREE.Plane(new THREE.Vector3(0, -1, 0), 0), + new THREE.Plane(new THREE.Vector3(0, 0, -1), 0), +]; + +init(); +render(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.localClippingEnabled = true; + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 200); + + camera.position.set(-1.5, 2.5, 3.0); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use only if there is no animation loop + controls.minDistance = 1; + controls.maxDistance = 10; + controls.enablePan = false; + + const light = new THREE.HemisphereLight(0xffffff, 0x080808, 4.5); + light.position.set(-1.25, 1, 1.25); + scene.add(light); + + // + + const group = new THREE.Group(); + + for (let i = 1; i <= 30; i += 2) { + const geometry = new THREE.SphereGeometry(i / 30, 48, 24); + + const material = new THREE.MeshPhongMaterial({ + color: new THREE.Color().setHSL(Math.random(), 0.5, 0.5, THREE.SRGBColorSpace), + side: THREE.DoubleSide, + clippingPlanes: clipPlanes, + clipIntersection: params.clipIntersection, + alphaToCoverage: true, + }); + + group.add(new THREE.Mesh(geometry, material)); + } + + scene.add(group); + + // helpers + + const helpers = new THREE.Group(); + helpers.add(new THREE.PlaneHelper(clipPlanes[0], 2, 0xff0000)); + helpers.add(new THREE.PlaneHelper(clipPlanes[1], 2, 0x00ff00)); + helpers.add(new THREE.PlaneHelper(clipPlanes[2], 2, 0x0000ff)); + helpers.visible = false; + scene.add(helpers); + + // gui + + const gui = new GUI(); + + gui.add(params, 'alphaToCoverage').onChange(function (value) { + group.children.forEach(c => { + c.material.alphaToCoverage = Boolean(value); + c.material.needsUpdate = true; + }); + + render(); + }); + + gui.add(params, 'clipIntersection') + .name('clip intersection') + .onChange(function (value) { + const children = group.children; + + for (let i = 0; i < children.length; i++) { + children[i].material.clipIntersection = value; + } + + render(); + }); + + gui.add(params, 'planeConstant', -1, 1) + .step(0.01) + .name('plane constant') + .onChange(function (value) { + for (let j = 0; j < clipPlanes.length; j++) { + clipPlanes[j].constant = value; + } + + render(); + }); + + gui.add(params, 'showHelpers') + .name('show helpers') + .onChange(function (value) { + helpers.visible = value; + + render(); + }); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_clipping_stencil.ts b/examples-testing/examples/webgl_clipping_stencil.ts new file mode 100644 index 000000000..ecb6b42b8 --- /dev/null +++ b/examples-testing/examples/webgl_clipping_stencil.ts @@ -0,0 +1,260 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import Stats from 'three/addons/libs/stats.module.js'; + +let camera, scene, renderer, object, stats; +let planes, planeObjects, planeHelpers; +let clock; + +const params = { + animate: true, + planeX: { + constant: 0, + negated: false, + displayHelper: false, + }, + planeY: { + constant: 0, + negated: false, + displayHelper: false, + }, + planeZ: { + constant: 0, + negated: false, + displayHelper: false, + }, +}; + +init(); + +function createPlaneStencilGroup(geometry, plane, renderOrder) { + const group = new THREE.Group(); + const baseMat = new THREE.MeshBasicMaterial(); + baseMat.depthWrite = false; + baseMat.depthTest = false; + baseMat.colorWrite = false; + baseMat.stencilWrite = true; + baseMat.stencilFunc = THREE.AlwaysStencilFunc; + + // back faces + const mat0 = baseMat.clone(); + mat0.side = THREE.BackSide; + mat0.clippingPlanes = [plane]; + mat0.stencilFail = THREE.IncrementWrapStencilOp; + mat0.stencilZFail = THREE.IncrementWrapStencilOp; + mat0.stencilZPass = THREE.IncrementWrapStencilOp; + + const mesh0 = new THREE.Mesh(geometry, mat0); + mesh0.renderOrder = renderOrder; + group.add(mesh0); + + // front faces + const mat1 = baseMat.clone(); + mat1.side = THREE.FrontSide; + mat1.clippingPlanes = [plane]; + mat1.stencilFail = THREE.DecrementWrapStencilOp; + mat1.stencilZFail = THREE.DecrementWrapStencilOp; + mat1.stencilZPass = THREE.DecrementWrapStencilOp; + + const mesh1 = new THREE.Mesh(geometry, mat1); + mesh1.renderOrder = renderOrder; + + group.add(mesh1); + + return group; +} + +function init() { + clock = new THREE.Clock(); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(36, window.innerWidth / window.innerHeight, 1, 100); + camera.position.set(2, 2, 2); + + scene.add(new THREE.AmbientLight(0xffffff, 1.5)); + + const dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.position.set(5, 10, 7.5); + dirLight.castShadow = true; + dirLight.shadow.camera.right = 2; + dirLight.shadow.camera.left = -2; + dirLight.shadow.camera.top = 2; + dirLight.shadow.camera.bottom = -2; + + dirLight.shadow.mapSize.width = 1024; + dirLight.shadow.mapSize.height = 1024; + scene.add(dirLight); + + planes = [ + new THREE.Plane(new THREE.Vector3(-1, 0, 0), 0), + new THREE.Plane(new THREE.Vector3(0, -1, 0), 0), + new THREE.Plane(new THREE.Vector3(0, 0, -1), 0), + ]; + + planeHelpers = planes.map(p => new THREE.PlaneHelper(p, 2, 0xffffff)); + planeHelpers.forEach(ph => { + ph.visible = false; + scene.add(ph); + }); + + const geometry = new THREE.TorusKnotGeometry(0.4, 0.15, 220, 60); + object = new THREE.Group(); + scene.add(object); + + // Set up clip plane rendering + planeObjects = []; + const planeGeom = new THREE.PlaneGeometry(4, 4); + + for (let i = 0; i < 3; i++) { + const poGroup = new THREE.Group(); + const plane = planes[i]; + const stencilGroup = createPlaneStencilGroup(geometry, plane, i + 1); + + // plane is clipped by the other clipping planes + const planeMat = new THREE.MeshStandardMaterial({ + color: 0xe91e63, + metalness: 0.1, + roughness: 0.75, + clippingPlanes: planes.filter(p => p !== plane), + + stencilWrite: true, + stencilRef: 0, + stencilFunc: THREE.NotEqualStencilFunc, + stencilFail: THREE.ReplaceStencilOp, + stencilZFail: THREE.ReplaceStencilOp, + stencilZPass: THREE.ReplaceStencilOp, + }); + const po = new THREE.Mesh(planeGeom, planeMat); + po.onAfterRender = function (renderer) { + renderer.clearStencil(); + }; + + po.renderOrder = i + 1.1; + + object.add(stencilGroup); + poGroup.add(po); + planeObjects.push(po); + scene.add(poGroup); + } + + const material = new THREE.MeshStandardMaterial({ + color: 0xffc107, + metalness: 0.1, + roughness: 0.75, + clippingPlanes: planes, + clipShadows: true, + shadowSide: THREE.DoubleSide, + }); + + // add the color + const clippedColorFront = new THREE.Mesh(geometry, material); + clippedColorFront.castShadow = true; + clippedColorFront.renderOrder = 6; + object.add(clippedColorFront); + + const ground = new THREE.Mesh( + new THREE.PlaneGeometry(9, 9, 1, 1), + new THREE.ShadowMaterial({ color: 0x000000, opacity: 0.25, side: THREE.DoubleSide }), + ); + + ground.rotation.x = -Math.PI / 2; // rotates X/Y to X/Z + ground.position.y = -1; + ground.receiveShadow = true; + scene.add(ground); + + // Renderer + renderer = new THREE.WebGLRenderer({ antialias: true, stencil: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setClearColor(0x263238); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + renderer.localClippingEnabled = true; + document.body.appendChild(renderer.domElement); + + // Stats + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); + + // Controls + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 2; + controls.maxDistance = 20; + controls.update(); + + // GUI + const gui = new GUI(); + gui.add(params, 'animate'); + + const planeX = gui.addFolder('planeX'); + planeX.add(params.planeX, 'displayHelper').onChange(v => (planeHelpers[0].visible = v)); + planeX + .add(params.planeX, 'constant') + .min(-1) + .max(1) + .onChange(d => (planes[0].constant = d)); + planeX.add(params.planeX, 'negated').onChange(() => { + planes[0].negate(); + params.planeX.constant = planes[0].constant; + }); + planeX.open(); + + const planeY = gui.addFolder('planeY'); + planeY.add(params.planeY, 'displayHelper').onChange(v => (planeHelpers[1].visible = v)); + planeY + .add(params.planeY, 'constant') + .min(-1) + .max(1) + .onChange(d => (planes[1].constant = d)); + planeY.add(params.planeY, 'negated').onChange(() => { + planes[1].negate(); + params.planeY.constant = planes[1].constant; + }); + planeY.open(); + + const planeZ = gui.addFolder('planeZ'); + planeZ.add(params.planeZ, 'displayHelper').onChange(v => (planeHelpers[2].visible = v)); + planeZ + .add(params.planeZ, 'constant') + .min(-1) + .max(1) + .onChange(d => (planes[2].constant = d)); + planeZ.add(params.planeZ, 'negated').onChange(() => { + planes[2].negate(); + params.planeZ.constant = planes[2].constant; + }); + planeZ.open(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const delta = clock.getDelta(); + + if (params.animate) { + object.rotation.x += delta * 0.5; + object.rotation.y += delta * 0.2; + } + + for (let i = 0; i < planeObjects.length; i++) { + const plane = planes[i]; + const po = planeObjects[i]; + plane.coplanarPoint(po.position); + po.lookAt(po.position.x - plane.normal.x, po.position.y - plane.normal.y, po.position.z - plane.normal.z); + } + + stats.begin(); + renderer.render(scene, camera); + stats.end(); +} diff --git a/examples-testing/examples/webgl_custom_attributes.ts b/examples-testing/examples/webgl_custom_attributes.ts new file mode 100644 index 000000000..0dc897748 --- /dev/null +++ b/examples-testing/examples/webgl_custom_attributes.ts @@ -0,0 +1,100 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let renderer, scene, camera, stats; + +let sphere, uniforms; + +let displacement, noise; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 300; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050505); + + uniforms = { + amplitude: { value: 1.0 }, + color: { value: new THREE.Color(0xff2200) }, + colorTexture: { value: new THREE.TextureLoader().load('textures/water.jpg') }, + }; + + uniforms['colorTexture'].value.wrapS = uniforms['colorTexture'].value.wrapT = THREE.RepeatWrapping; + + const shaderMaterial = new THREE.ShaderMaterial({ + uniforms: uniforms, + vertexShader: document.getElementById('vertexshader').textContent, + fragmentShader: document.getElementById('fragmentshader').textContent, + }); + + const radius = 50, + segments = 128, + rings = 64; + + const geometry = new THREE.SphereGeometry(radius, segments, rings); + + displacement = new Float32Array(geometry.attributes.position.count); + noise = new Float32Array(geometry.attributes.position.count); + + for (let i = 0; i < displacement.length; i++) { + noise[i] = Math.random() * 5; + } + + geometry.setAttribute('displacement', new THREE.BufferAttribute(displacement, 1)); + + sphere = new THREE.Mesh(geometry, shaderMaterial); + scene.add(sphere); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + + const container = document.getElementById('container'); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + const time = Date.now() * 0.01; + + sphere.rotation.y = sphere.rotation.z = 0.01 * time; + + uniforms['amplitude'].value = 2.5 * Math.sin(sphere.rotation.y * 0.125); + uniforms['color'].value.offsetHSL(0.0005, 0, 0); + + for (let i = 0; i < displacement.length; i++) { + displacement[i] = Math.sin(0.1 * i + time); + + noise[i] += 0.5 * (0.5 - Math.random()); + noise[i] = THREE.MathUtils.clamp(noise[i], -5, 5); + + displacement[i] += noise[i]; + } + + sphere.geometry.attributes.displacement.needsUpdate = true; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_custom_attributes_lines.ts b/examples-testing/examples/webgl_custom_attributes_lines.ts new file mode 100644 index 000000000..3e2454e92 --- /dev/null +++ b/examples-testing/examples/webgl_custom_attributes_lines.ts @@ -0,0 +1,121 @@ +import * as THREE from 'three'; + +import { FontLoader } from 'three/addons/loaders/FontLoader.js'; +import { TextGeometry } from 'three/addons/geometries/TextGeometry.js'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let renderer, scene, camera, stats; + +let line, uniforms; + +const loader = new FontLoader(); +loader.load('fonts/helvetiker_bold.typeface.json', function (font) { + init(font); +}); + +function init(font) { + camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 400; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050505); + + uniforms = { + amplitude: { value: 5.0 }, + opacity: { value: 0.3 }, + color: { value: new THREE.Color(0xffffff) }, + }; + + const shaderMaterial = new THREE.ShaderMaterial({ + uniforms: uniforms, + vertexShader: document.getElementById('vertexshader').textContent, + fragmentShader: document.getElementById('fragmentshader').textContent, + blending: THREE.AdditiveBlending, + depthTest: false, + transparent: true, + }); + + const geometry = new TextGeometry('three.js', { + font: font, + + size: 50, + depth: 15, + curveSegments: 10, + + bevelThickness: 5, + bevelSize: 1.5, + bevelEnabled: true, + bevelSegments: 10, + }); + + geometry.center(); + + const count = geometry.attributes.position.count; + + const displacement = new THREE.Float32BufferAttribute(count * 3, 3); + geometry.setAttribute('displacement', displacement); + + const customColor = new THREE.Float32BufferAttribute(count * 3, 3); + geometry.setAttribute('customColor', customColor); + + const color = new THREE.Color(0xffffff); + + for (let i = 0, l = customColor.count; i < l; i++) { + color.setHSL(i / l, 0.5, 0.5); + color.toArray(customColor.array, i * customColor.itemSize); + } + + line = new THREE.Line(geometry, shaderMaterial); + line.rotation.x = 0.2; + scene.add(line); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + + const container = document.getElementById('container'); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + const time = Date.now() * 0.001; + + line.rotation.y = 0.25 * time; + + uniforms.amplitude.value = Math.sin(0.5 * time); + uniforms.color.value.offsetHSL(0.0005, 0, 0); + + const attributes = line.geometry.attributes; + const array = attributes.displacement.array; + + for (let i = 0, l = array.length; i < l; i += 3) { + array[i] += 0.3 * (0.5 - Math.random()); + array[i + 1] += 0.3 * (0.5 - Math.random()); + array[i + 2] += 0.3 * (0.5 - Math.random()); + } + + attributes.displacement.needsUpdate = true; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_custom_attributes_points.ts b/examples-testing/examples/webgl_custom_attributes_points.ts new file mode 100644 index 000000000..ae112980a --- /dev/null +++ b/examples-testing/examples/webgl_custom_attributes_points.ts @@ -0,0 +1,117 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let renderer, scene, camera, stats; + +let sphere; + +const WIDTH = window.innerWidth; +const HEIGHT = window.innerHeight; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(40, WIDTH / HEIGHT, 1, 10000); + camera.position.z = 300; + + scene = new THREE.Scene(); + + const amount = 100000; + const radius = 200; + + const positions = new Float32Array(amount * 3); + const colors = new Float32Array(amount * 3); + const sizes = new Float32Array(amount); + + const vertex = new THREE.Vector3(); + const color = new THREE.Color(0xffffff); + + for (let i = 0; i < amount; i++) { + vertex.x = (Math.random() * 2 - 1) * radius; + vertex.y = (Math.random() * 2 - 1) * radius; + vertex.z = (Math.random() * 2 - 1) * radius; + vertex.toArray(positions, i * 3); + + if (vertex.x < 0) { + color.setHSL(0.5 + 0.1 * (i / amount), 0.7, 0.5); + } else { + color.setHSL(0.0 + 0.1 * (i / amount), 0.9, 0.5); + } + + color.toArray(colors, i * 3); + + sizes[i] = 10; + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('customColor', new THREE.BufferAttribute(colors, 3)); + geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); + + // + + const material = new THREE.ShaderMaterial({ + uniforms: { + color: { value: new THREE.Color(0xffffff) }, + pointTexture: { value: new THREE.TextureLoader().load('textures/sprites/spark1.png') }, + }, + vertexShader: document.getElementById('vertexshader').textContent, + fragmentShader: document.getElementById('fragmentshader').textContent, + + blending: THREE.AdditiveBlending, + depthTest: false, + transparent: true, + }); + + // + + sphere = new THREE.Points(geometry, material); + scene.add(sphere); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(WIDTH, HEIGHT); + renderer.setAnimationLoop(animate); + + const container = document.getElementById('container'); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + const time = Date.now() * 0.005; + + sphere.rotation.z = 0.01 * time; + + const geometry = sphere.geometry; + const attributes = geometry.attributes; + + for (let i = 0; i < attributes.size.array.length; i++) { + attributes.size.array[i] = 14 + 13 * Math.sin(0.1 * i + time); + } + + attributes.size.needsUpdate = true; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_custom_attributes_points2.ts b/examples-testing/examples/webgl_custom_attributes_points2.ts new file mode 100644 index 000000000..edd158fa1 --- /dev/null +++ b/examples-testing/examples/webgl_custom_attributes_points2.ts @@ -0,0 +1,193 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; + +let renderer, scene, camera, stats; +let sphere, length1; + +const WIDTH = window.innerWidth; +const HEIGHT = window.innerHeight; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(45, WIDTH / HEIGHT, 1, 10000); + camera.position.z = 300; + + scene = new THREE.Scene(); + + const radius = 100, + segments = 68, + rings = 38; + + let sphereGeometry = new THREE.SphereGeometry(radius, segments, rings); + let boxGeometry = new THREE.BoxGeometry(0.8 * radius, 0.8 * radius, 0.8 * radius, 10, 10, 10); + + // if normal and uv attributes are not removed, mergeVertices() can't consolidate identical vertices with different normal/uv data + + sphereGeometry.deleteAttribute('normal'); + sphereGeometry.deleteAttribute('uv'); + + boxGeometry.deleteAttribute('normal'); + boxGeometry.deleteAttribute('uv'); + + sphereGeometry = BufferGeometryUtils.mergeVertices(sphereGeometry); + boxGeometry = BufferGeometryUtils.mergeVertices(boxGeometry); + + const combinedGeometry = BufferGeometryUtils.mergeGeometries([sphereGeometry, boxGeometry]); + const positionAttribute = combinedGeometry.getAttribute('position'); + + const colors = []; + const sizes = []; + + const color = new THREE.Color(); + const vertex = new THREE.Vector3(); + + length1 = sphereGeometry.getAttribute('position').count; + + for (let i = 0, l = positionAttribute.count; i < l; i++) { + vertex.fromBufferAttribute(positionAttribute, i); + + if (i < length1) { + color.setHSL(0.01 + 0.1 * (i / length1), 0.99, (vertex.y + radius) / (4 * radius)); + } else { + color.setHSL(0.6, 0.75, 0.25 + vertex.y / (2 * radius)); + } + + color.toArray(colors, i * 3); + + sizes[i] = i < length1 ? 10 : 40; + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', positionAttribute); + geometry.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1)); + geometry.setAttribute('ca', new THREE.Float32BufferAttribute(colors, 3)); + + // + + const texture = new THREE.TextureLoader().load('textures/sprites/disc.png'); + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + + const material = new THREE.ShaderMaterial({ + uniforms: { + color: { value: new THREE.Color(0xffffff) }, + pointTexture: { value: texture }, + }, + vertexShader: document.getElementById('vertexshader').textContent, + fragmentShader: document.getElementById('fragmentshader').textContent, + transparent: true, + }); + + // + + sphere = new THREE.Points(geometry, material); + scene.add(sphere); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(WIDTH, HEIGHT); + renderer.setAnimationLoop(animate); + + const container = document.getElementById('container'); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function sortPoints() { + const vector = new THREE.Vector3(); + + // Model View Projection matrix + + const matrix = new THREE.Matrix4(); + matrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); + matrix.multiply(sphere.matrixWorld); + + // + + const geometry = sphere.geometry; + + let index = geometry.getIndex(); + const positions = geometry.getAttribute('position').array; + const length = positions.length / 3; + + if (index === null) { + const array = new Uint16Array(length); + + for (let i = 0; i < length; i++) { + array[i] = i; + } + + index = new THREE.BufferAttribute(array, 1); + + geometry.setIndex(index); + } + + const sortArray = []; + + for (let i = 0; i < length; i++) { + vector.fromArray(positions, i * 3); + vector.applyMatrix4(matrix); + + sortArray.push([vector.z, i]); + } + + function numericalSort(a, b) { + return b[0] - a[0]; + } + + sortArray.sort(numericalSort); + + const indices = index.array; + + for (let i = 0; i < length; i++) { + indices[i] = sortArray[i][1]; + } + + geometry.index.needsUpdate = true; +} + +function animate() { + render(); + stats.update(); +} + +function render() { + const time = Date.now() * 0.005; + + sphere.rotation.y = 0.02 * time; + sphere.rotation.z = 0.02 * time; + + const geometry = sphere.geometry; + const attributes = geometry.attributes; + + for (let i = 0; i < attributes.size.array.length; i++) { + if (i < length1) { + attributes.size.array[i] = 16 + 12 * Math.sin(0.1 * i + time); + } + } + + attributes.size.needsUpdate = true; + + sortPoints(); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_custom_attributes_points3.ts b/examples-testing/examples/webgl_custom_attributes_points3.ts new file mode 100644 index 000000000..1b46a805e --- /dev/null +++ b/examples-testing/examples/webgl_custom_attributes_points3.ts @@ -0,0 +1,200 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; + +let renderer, scene, camera, stats; + +let object; + +let vertices1; + +const WIDTH = window.innerWidth; +const HEIGHT = window.innerHeight; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(40, WIDTH / HEIGHT, 1, 1000); + camera.position.z = 500; + + scene = new THREE.Scene(); + + let radius = 100; + const inner = 0.6 * radius; + const vertex = new THREE.Vector3(); + const vertices = []; + + for (let i = 0; i < 100000; i++) { + vertex.x = Math.random() * 2 - 1; + vertex.y = Math.random() * 2 - 1; + vertex.z = Math.random() * 2 - 1; + vertex.multiplyScalar(radius); + + if ( + vertex.x > inner || + vertex.x < -inner || + vertex.y > inner || + vertex.y < -inner || + vertex.z > inner || + vertex.z < -inner + ) + vertices.push(vertex.x, vertex.y, vertex.z); + } + + vertices1 = vertices.length / 3; + + radius = 200; + + let boxGeometry1 = new THREE.BoxGeometry(radius, 0.1 * radius, 0.1 * radius, 50, 5, 5); + + // if normal and uv attributes are not removed, mergeVertices() can't consolidate indentical vertices with different normal/uv data + + boxGeometry1.deleteAttribute('normal'); + boxGeometry1.deleteAttribute('uv'); + + boxGeometry1 = BufferGeometryUtils.mergeVertices(boxGeometry1); + + const matrix = new THREE.Matrix4(); + const position = new THREE.Vector3(); + const rotation = new THREE.Euler(); + const quaternion = new THREE.Quaternion(); + const scale = new THREE.Vector3(1, 1, 1); + + function addGeo(geo, x, y, z, ry) { + position.set(x, y, z); + rotation.set(0, ry, 0); + + matrix.compose(position, quaternion.setFromEuler(rotation), scale); + + const positionAttribute = geo.getAttribute('position'); + + for (let i = 0, l = positionAttribute.count; i < l; i++) { + vertex.fromBufferAttribute(positionAttribute, i); + vertex.applyMatrix4(matrix); + vertices.push(vertex.x, vertex.y, vertex.z); + } + } + + // side 1 + + addGeo(boxGeometry1, 0, 110, 110, 0); + addGeo(boxGeometry1, 0, 110, -110, 0); + addGeo(boxGeometry1, 0, -110, 110, 0); + addGeo(boxGeometry1, 0, -110, -110, 0); + + // side 2 + + addGeo(boxGeometry1, 110, 110, 0, Math.PI / 2); + addGeo(boxGeometry1, 110, -110, 0, Math.PI / 2); + addGeo(boxGeometry1, -110, 110, 0, Math.PI / 2); + addGeo(boxGeometry1, -110, -110, 0, Math.PI / 2); + + // corner edges + + let boxGeometry2 = new THREE.BoxGeometry(0.1 * radius, radius * 1.2, 0.1 * radius, 5, 60, 5); + + boxGeometry2.deleteAttribute('normal'); + boxGeometry2.deleteAttribute('uv'); + + boxGeometry2 = BufferGeometryUtils.mergeVertices(boxGeometry2); + + addGeo(boxGeometry2, 110, 0, 110, 0); + addGeo(boxGeometry2, 110, 0, -110, 0); + addGeo(boxGeometry2, -110, 0, 110, 0); + addGeo(boxGeometry2, -110, 0, -110, 0); + + const positionAttribute = new THREE.Float32BufferAttribute(vertices, 3); + + const colors = []; + const sizes = []; + + const color = new THREE.Color(); + + for (let i = 0; i < positionAttribute.count; i++) { + if (i < vertices1) { + color.setHSL(0.5 + 0.2 * (i / vertices1), 1, 0.5); + } else { + color.setHSL(0.1, 1, 0.5); + } + + color.toArray(colors, i * 3); + + sizes[i] = i < vertices1 ? 10 : 40; + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', positionAttribute); + geometry.setAttribute('ca', new THREE.Float32BufferAttribute(colors, 3)); + geometry.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1)); + + // + + const texture = new THREE.TextureLoader().load('textures/sprites/ball.png'); + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + + const material = new THREE.ShaderMaterial({ + uniforms: { + amplitude: { value: 1.0 }, + color: { value: new THREE.Color(0xffffff) }, + pointTexture: { value: texture }, + }, + vertexShader: document.getElementById('vertexshader').textContent, + fragmentShader: document.getElementById('fragmentshader').textContent, + }); + + // + + object = new THREE.Points(geometry, material); + scene.add(object); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(WIDTH, HEIGHT); + renderer.setAnimationLoop(animate); + + const container = document.getElementById('container'); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + const time = Date.now() * 0.01; + + object.rotation.y = object.rotation.z = 0.02 * time; + + const geometry = object.geometry; + const attributes = geometry.attributes; + + for (let i = 0; i < attributes.size.array.length; i++) { + if (i < vertices1) { + attributes.size.array[i] = Math.max(0, 26 + 32 * Math.sin(0.1 * i + 0.6 * time)); + } + } + + attributes.size.needsUpdate = true; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_decals.ts b/examples-testing/examples/webgl_decals.ts new file mode 100644 index 000000000..23cdb4da8 --- /dev/null +++ b/examples-testing/examples/webgl_decals.ts @@ -0,0 +1,237 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { DecalGeometry } from 'three/addons/geometries/DecalGeometry.js'; + +const container = document.getElementById('container'); + +let renderer, scene, camera, stats; +let mesh; +let raycaster; +let line; + +const intersection = { + intersects: false, + point: new THREE.Vector3(), + normal: new THREE.Vector3(), +}; +const mouse = new THREE.Vector2(); +const intersects = []; + +const textureLoader = new THREE.TextureLoader(); +const decalDiffuse = textureLoader.load('textures/decal/decal-diffuse.png'); +decalDiffuse.colorSpace = THREE.SRGBColorSpace; +const decalNormal = textureLoader.load('textures/decal/decal-normal.jpg'); + +const decalMaterial = new THREE.MeshPhongMaterial({ + specular: 0x444444, + map: decalDiffuse, + normalMap: decalNormal, + normalScale: new THREE.Vector2(1, 1), + shininess: 30, + transparent: true, + depthTest: true, + depthWrite: false, + polygonOffset: true, + polygonOffsetFactor: -4, + wireframe: false, +}); + +const decals = []; +let mouseHelper; +const position = new THREE.Vector3(); +const orientation = new THREE.Euler(); +const size = new THREE.Vector3(10, 10, 10); + +const params = { + minScale: 10, + maxScale: 20, + rotate: true, + clear: function () { + removeDecals(); + }, +}; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 120; + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 50; + controls.maxDistance = 200; + + scene.add(new THREE.AmbientLight(0x666666)); + + const dirLight1 = new THREE.DirectionalLight(0xffddcc, 3); + dirLight1.position.set(1, 0.75, 0.5); + scene.add(dirLight1); + + const dirLight2 = new THREE.DirectionalLight(0xccccff, 3); + dirLight2.position.set(-1, 0.75, -0.5); + scene.add(dirLight2); + + const geometry = new THREE.BufferGeometry(); + geometry.setFromPoints([new THREE.Vector3(), new THREE.Vector3()]); + + line = new THREE.Line(geometry, new THREE.LineBasicMaterial()); + scene.add(line); + + loadLeePerrySmith(); + + raycaster = new THREE.Raycaster(); + + mouseHelper = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 10), new THREE.MeshNormalMaterial()); + mouseHelper.visible = false; + scene.add(mouseHelper); + + window.addEventListener('resize', onWindowResize); + + let moved = false; + + controls.addEventListener('change', function () { + moved = true; + }); + + window.addEventListener('pointerdown', function () { + moved = false; + }); + + window.addEventListener('pointerup', function (event) { + if (moved === false) { + checkIntersection(event.clientX, event.clientY); + + if (intersection.intersects) shoot(); + } + }); + + window.addEventListener('pointermove', onPointerMove); + + function onPointerMove(event) { + if (event.isPrimary) { + checkIntersection(event.clientX, event.clientY); + } + } + + function checkIntersection(x, y) { + if (mesh === undefined) return; + + mouse.x = (x / window.innerWidth) * 2 - 1; + mouse.y = -(y / window.innerHeight) * 2 + 1; + + raycaster.setFromCamera(mouse, camera); + raycaster.intersectObject(mesh, false, intersects); + + if (intersects.length > 0) { + const p = intersects[0].point; + mouseHelper.position.copy(p); + intersection.point.copy(p); + + const n = intersects[0].face.normal.clone(); + n.transformDirection(mesh.matrixWorld); + n.multiplyScalar(10); + n.add(intersects[0].point); + + intersection.normal.copy(intersects[0].face.normal); + mouseHelper.lookAt(n); + + const positions = line.geometry.attributes.position; + positions.setXYZ(0, p.x, p.y, p.z); + positions.setXYZ(1, n.x, n.y, n.z); + positions.needsUpdate = true; + + intersection.intersects = true; + + intersects.length = 0; + } else { + intersection.intersects = false; + } + } + + const gui = new GUI(); + + gui.add(params, 'minScale', 1, 30); + gui.add(params, 'maxScale', 1, 30); + gui.add(params, 'rotate'); + gui.add(params, 'clear'); + gui.open(); +} + +function loadLeePerrySmith() { + const map = textureLoader.load('models/gltf/LeePerrySmith/Map-COL.jpg'); + map.colorSpace = THREE.SRGBColorSpace; + const specularMap = textureLoader.load('models/gltf/LeePerrySmith/Map-SPEC.jpg'); + const normalMap = textureLoader.load('models/gltf/LeePerrySmith/Infinite-Level_02_Tangent_SmoothUV.jpg'); + + const loader = new GLTFLoader(); + + loader.load('models/gltf/LeePerrySmith/LeePerrySmith.glb', function (gltf) { + mesh = gltf.scene.children[0]; + mesh.material = new THREE.MeshPhongMaterial({ + specular: 0x111111, + map: map, + specularMap: specularMap, + normalMap: normalMap, + shininess: 25, + }); + + scene.add(mesh); + mesh.scale.set(10, 10, 10); + }); +} + +function shoot() { + position.copy(intersection.point); + orientation.copy(mouseHelper.rotation); + + if (params.rotate) orientation.z = Math.random() * 2 * Math.PI; + + const scale = params.minScale + Math.random() * (params.maxScale - params.minScale); + size.set(scale, scale, scale); + + const material = decalMaterial.clone(); + material.color.setHex(Math.random() * 0xffffff); + + const m = new THREE.Mesh(new DecalGeometry(mesh, position, orientation, size), material); + m.renderOrder = decals.length; // give decals a fixed render order + + decals.push(m); + scene.add(m); +} + +function removeDecals() { + decals.forEach(function (d) { + scene.remove(d); + }); + + decals.length = 0; +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_effects_anaglyph.ts b/examples-testing/examples/webgl_effects_anaglyph.ts new file mode 100644 index 000000000..8415973df --- /dev/null +++ b/examples-testing/examples/webgl_effects_anaglyph.ts @@ -0,0 +1,114 @@ +import * as THREE from 'three'; + +import { AnaglyphEffect } from 'three/addons/effects/AnaglyphEffect.js'; + +let container, camera, scene, renderer, effect; + +const spheres = []; + +let mouseX = 0; +let mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +document.addEventListener('mousemove', onDocumentMouseMove); + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.01, 100); + camera.position.z = 3; + + const path = 'textures/cube/pisa/'; + const format = '.png'; + const urls = [ + path + 'px' + format, + path + 'nx' + format, + path + 'py' + format, + path + 'ny' + format, + path + 'pz' + format, + path + 'nz' + format, + ]; + + const textureCube = new THREE.CubeTextureLoader().load(urls); + + scene = new THREE.Scene(); + scene.background = textureCube; + + const geometry = new THREE.SphereGeometry(0.1, 32, 16); + const material = new THREE.MeshBasicMaterial({ color: 0xffffff, envMap: textureCube }); + + for (let i = 0; i < 500; i++) { + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.x = Math.random() * 10 - 5; + mesh.position.y = Math.random() * 10 - 5; + mesh.position.z = Math.random() * 10 - 5; + + mesh.scale.x = mesh.scale.y = mesh.scale.z = Math.random() * 3 + 1; + + scene.add(mesh); + + spheres.push(mesh); + } + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + const width = window.innerWidth || 2; + const height = window.innerHeight || 2; + + effect = new AnaglyphEffect(renderer); + effect.setSize(width, height); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + effect.setSize(window.innerWidth, window.innerHeight); +} + +function onDocumentMouseMove(event) { + mouseX = (event.clientX - windowHalfX) / 100; + mouseY = (event.clientY - windowHalfY) / 100; +} + +// + +function animate() { + render(); +} + +function render() { + const timer = 0.0001 * Date.now(); + + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-mouseY - camera.position.y) * 0.05; + + camera.lookAt(scene.position); + + for (let i = 0, il = spheres.length; i < il; i++) { + const sphere = spheres[i]; + + sphere.position.x = 5 * Math.cos(timer + i); + sphere.position.y = 5 * Math.sin(timer + i * 1.1); + } + + effect.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_effects_ascii.ts b/examples-testing/examples/webgl_effects_ascii.ts new file mode 100644 index 000000000..a412bb79e --- /dev/null +++ b/examples-testing/examples/webgl_effects_ascii.ts @@ -0,0 +1,81 @@ +import * as THREE from 'three'; + +import { AsciiEffect } from 'three/addons/effects/AsciiEffect.js'; +import { TrackballControls } from 'three/addons/controls/TrackballControls.js'; + +let camera, controls, scene, renderer, effect; + +let sphere, plane; + +const start = Date.now(); + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.y = 150; + camera.position.z = 500; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0, 0, 0); + + const pointLight1 = new THREE.PointLight(0xffffff, 3, 0, 0); + pointLight1.position.set(500, 500, 500); + scene.add(pointLight1); + + const pointLight2 = new THREE.PointLight(0xffffff, 1, 0, 0); + pointLight2.position.set(-500, -500, -500); + scene.add(pointLight2); + + sphere = new THREE.Mesh(new THREE.SphereGeometry(200, 20, 10), new THREE.MeshPhongMaterial({ flatShading: true })); + scene.add(sphere); + + // Plane + + plane = new THREE.Mesh(new THREE.PlaneGeometry(400, 400), new THREE.MeshBasicMaterial({ color: 0xe0e0e0 })); + plane.position.y = -200; + plane.rotation.x = -Math.PI / 2; + scene.add(plane); + + renderer = new THREE.WebGLRenderer(); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + + effect = new AsciiEffect(renderer, ' .:-+*=%@#', { invert: true }); + effect.setSize(window.innerWidth, window.innerHeight); + effect.domElement.style.color = 'white'; + effect.domElement.style.backgroundColor = 'black'; + + // Special case: append effect.domElement, instead of renderer.domElement. + // AsciiEffect creates a custom domElement (a div container) where the ASCII elements are placed. + + document.body.appendChild(effect.domElement); + + controls = new TrackballControls(camera, effect.domElement); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + effect.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const timer = Date.now() - start; + + sphere.position.y = Math.abs(Math.sin(timer * 0.002)) * 150; + sphere.rotation.x = timer * 0.0003; + sphere.rotation.z = timer * 0.0002; + + controls.update(); + + effect.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_effects_parallaxbarrier.ts b/examples-testing/examples/webgl_effects_parallaxbarrier.ts new file mode 100644 index 000000000..90c867973 --- /dev/null +++ b/examples-testing/examples/webgl_effects_parallaxbarrier.ts @@ -0,0 +1,110 @@ +import * as THREE from 'three'; + +import { ParallaxBarrierEffect } from 'three/addons/effects/ParallaxBarrierEffect.js'; + +let container, camera, scene, renderer, effect; + +const spheres = []; + +let mouseX = 0; +let mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +document.addEventListener('mousemove', onDocumentMouseMove); + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.01, 100); + camera.position.z = 3; + + const path = 'textures/cube/pisa/'; + const format = '.png'; + const urls = [ + path + 'px' + format, + path + 'nx' + format, + path + 'py' + format, + path + 'ny' + format, + path + 'pz' + format, + path + 'nz' + format, + ]; + + const textureCube = new THREE.CubeTextureLoader().load(urls); + + scene = new THREE.Scene(); + scene.background = textureCube; + + const geometry = new THREE.SphereGeometry(0.1, 32, 16); + const material = new THREE.MeshBasicMaterial({ color: 0xffffff, envMap: textureCube }); + + for (let i = 0; i < 500; i++) { + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.x = Math.random() * 10 - 5; + mesh.position.y = Math.random() * 10 - 5; + mesh.position.z = Math.random() * 10 - 5; + + mesh.scale.x = mesh.scale.y = mesh.scale.z = Math.random() * 3 + 1; + + scene.add(mesh); + + spheres.push(mesh); + } + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + const width = window.innerWidth || 2; + const height = window.innerHeight || 2; + + effect = new ParallaxBarrierEffect(renderer); + effect.setSize(width, height); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + effect.setSize(window.innerWidth, window.innerHeight); +} + +function onDocumentMouseMove(event) { + mouseX = (event.clientX - windowHalfX) / 100; + mouseY = (event.clientY - windowHalfY) / 100; +} + +// + +function animate() { + const timer = 0.0001 * Date.now(); + + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-mouseY - camera.position.y) * 0.05; + + camera.lookAt(scene.position); + + for (let i = 0, il = spheres.length; i < il; i++) { + const sphere = spheres[i]; + + sphere.position.x = 5 * Math.cos(timer + i); + sphere.position.y = 5 * Math.sin(timer + i * 1.1); + } + + effect.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_effects_peppersghost.ts b/examples-testing/examples/webgl_effects_peppersghost.ts new file mode 100644 index 000000000..41dfb4b65 --- /dev/null +++ b/examples-testing/examples/webgl_effects_peppersghost.ts @@ -0,0 +1,85 @@ +import * as THREE from 'three'; + +import { PeppersGhostEffect } from 'three/addons/effects/PeppersGhostEffect.js'; + +let container; + +let camera, scene, renderer, effect; +let group; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 100000); + + scene = new THREE.Scene(); + + group = new THREE.Group(); + scene.add(group); + + // Cube + + const geometry = new THREE.BoxGeometry().toNonIndexed(); // ensure unique vertices for each triangle + + const position = geometry.attributes.position; + const colors = []; + const color = new THREE.Color(); + + // generate for each side of the cube a different color + + for (let i = 0; i < position.count; i += 6) { + color.setHex(Math.random() * 0xffffff); + + // first face + + colors.push(color.r, color.g, color.b); + colors.push(color.r, color.g, color.b); + colors.push(color.r, color.g, color.b); + + // second face + + colors.push(color.r, color.g, color.b); + colors.push(color.r, color.g, color.b); + colors.push(color.r, color.g, color.b); + } + + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + + const material = new THREE.MeshBasicMaterial({ vertexColors: true }); + + for (let i = 0; i < 10; i++) { + const cube = new THREE.Mesh(geometry, material); + cube.position.x = Math.random() * 2 - 1; + cube.position.y = Math.random() * 2 - 1; + cube.position.z = Math.random() * 2 - 1; + cube.scale.multiplyScalar(Math.random() + 0.5); + group.add(cube); + } + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + effect = new PeppersGhostEffect(renderer); + effect.setSize(window.innerWidth, window.innerHeight); + effect.cameraDistance = 5; + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + effect.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + group.rotation.y += 0.01; + + effect.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_effects_stereo.ts b/examples-testing/examples/webgl_effects_stereo.ts new file mode 100644 index 000000000..dd2f61f91 --- /dev/null +++ b/examples-testing/examples/webgl_effects_stereo.ts @@ -0,0 +1,98 @@ +import * as THREE from 'three'; + +import { StereoEffect } from 'three/addons/effects/StereoEffect.js'; + +let container, camera, scene, renderer, effect; + +const spheres = []; + +let mouseX = 0, + mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +document.addEventListener('mousemove', onDocumentMouseMove); + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 100000); + camera.position.z = 3200; + + scene = new THREE.Scene(); + scene.background = new THREE.CubeTextureLoader() + .setPath('textures/cube/Park3Med/') + .load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']); + + const geometry = new THREE.SphereGeometry(100, 32, 16); + + const textureCube = new THREE.CubeTextureLoader() + .setPath('textures/cube/Park3Med/') + .load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']); + textureCube.mapping = THREE.CubeRefractionMapping; + + const material = new THREE.MeshBasicMaterial({ color: 0xffffff, envMap: textureCube, refractionRatio: 0.95 }); + + for (let i = 0; i < 500; i++) { + const mesh = new THREE.Mesh(geometry, material); + mesh.position.x = Math.random() * 10000 - 5000; + mesh.position.y = Math.random() * 10000 - 5000; + mesh.position.z = Math.random() * 10000 - 5000; + mesh.scale.x = mesh.scale.y = mesh.scale.z = Math.random() * 3 + 1; + scene.add(mesh); + + spheres.push(mesh); + } + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + effect = new StereoEffect(renderer); + effect.setSize(window.innerWidth, window.innerHeight); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + effect.setSize(window.innerWidth, window.innerHeight); +} + +function onDocumentMouseMove(event) { + mouseX = (event.clientX - windowHalfX) * 10; + mouseY = (event.clientY - windowHalfY) * 10; +} + +// + +function animate() { + const timer = 0.0001 * Date.now(); + + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-mouseY - camera.position.y) * 0.05; + camera.lookAt(scene.position); + + for (let i = 0, il = spheres.length; i < il; i++) { + const sphere = spheres[i]; + + sphere.position.x = 5000 * Math.cos(timer + i); + sphere.position.y = 5000 * Math.sin(timer + i * 1.1); + } + + effect.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_framebuffer_texture.ts b/examples-testing/examples/webgl_framebuffer_texture.ts new file mode 100644 index 000000000..df4acc9d6 --- /dev/null +++ b/examples-testing/examples/webgl_framebuffer_texture.ts @@ -0,0 +1,151 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import * as GeometryUtils from 'three/addons/utils/GeometryUtils.js'; + +let camera, scene, renderer; +let line, sprite, texture; + +let cameraOrtho, sceneOrtho; + +let offset = 0; + +const dpr = window.devicePixelRatio; + +const textureSize = 128 * dpr; +const vector = new THREE.Vector2(); +const color = new THREE.Color(); + +init(); + +function init() { + // + + const width = window.innerWidth; + const height = window.innerHeight; + + camera = new THREE.PerspectiveCamera(70, width / height, 1, 1000); + camera.position.z = 20; + + cameraOrtho = new THREE.OrthographicCamera(-width / 2, width / 2, height / 2, -height / 2, 1, 10); + cameraOrtho.position.z = 10; + + scene = new THREE.Scene(); + sceneOrtho = new THREE.Scene(); + + // + + const points = GeometryUtils.gosper(8); + + const geometry = new THREE.BufferGeometry(); + const positionAttribute = new THREE.Float32BufferAttribute(points, 3); + geometry.setAttribute('position', positionAttribute); + geometry.center(); + + const colorAttribute = new THREE.BufferAttribute(new Float32Array(positionAttribute.array.length), 3); + colorAttribute.setUsage(THREE.DynamicDrawUsage); + geometry.setAttribute('color', colorAttribute); + + const material = new THREE.LineBasicMaterial({ vertexColors: true }); + + line = new THREE.Line(geometry, material); + line.scale.setScalar(0.05); + scene.add(line); + + // + + texture = new THREE.FramebufferTexture(textureSize, textureSize); + + // + + const spriteMaterial = new THREE.SpriteMaterial({ map: texture }); + sprite = new THREE.Sprite(spriteMaterial); + sprite.scale.set(textureSize, textureSize, 1); + sceneOrtho.add(sprite); + + updateSpritePosition(); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.autoClear = false; + document.body.appendChild(renderer.domElement); + + // + + const selection = document.getElementById('selection'); + const controls = new OrbitControls(camera, selection); + controls.enablePan = false; + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + cameraOrtho.left = -width / 2; + cameraOrtho.right = width / 2; + cameraOrtho.top = height / 2; + cameraOrtho.bottom = -height / 2; + cameraOrtho.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + updateSpritePosition(); +} + +function updateSpritePosition() { + const halfWidth = window.innerWidth / 2; + const halfHeight = window.innerHeight / 2; + + const halfImageWidth = textureSize / 2; + const halfImageHeight = textureSize / 2; + + sprite.position.set(-halfWidth + halfImageWidth, halfHeight - halfImageHeight, 1); +} + +function animate() { + const colorAttribute = line.geometry.getAttribute('color'); + updateColors(colorAttribute); + + // scene rendering + + renderer.clear(); + renderer.render(scene, camera); + + // calculate start position for copying data + + vector.x = (window.innerWidth * dpr) / 2 - textureSize / 2; + vector.y = (window.innerHeight * dpr) / 2 - textureSize / 2; + + renderer.copyFramebufferToTexture(texture, vector); + + renderer.clearDepth(); + renderer.render(sceneOrtho, cameraOrtho); +} + +function updateColors(colorAttribute) { + const l = colorAttribute.count; + + for (let i = 0; i < l; i++) { + const h = ((offset + i) % l) / l; + + color.setHSL(h, 1, 0.5); + colorAttribute.setX(i, color.r); + colorAttribute.setY(i, color.g); + colorAttribute.setZ(i, color.b); + } + + colorAttribute.needsUpdate = true; + + offset -= 25; +} diff --git a/examples-testing/examples/webgl_furnace_test.ts b/examples-testing/examples/webgl_furnace_test.ts new file mode 100644 index 000000000..a81954176 --- /dev/null +++ b/examples-testing/examples/webgl_furnace_test.ts @@ -0,0 +1,96 @@ +import * as THREE from 'three'; + +let scene, camera, renderer, radianceMap; + +const COLOR = 0xcccccc; + +function init() { + const width = window.innerWidth; + const height = window.innerHeight; + const aspect = width / height; + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setSize(width, height); + renderer.setPixelRatio(window.devicePixelRatio); + document.body.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); + + document.body.addEventListener('mouseover', function () { + scene.traverse(function (child) { + if (child.isMesh) child.material.color.setHex(0xffffff); + }); + + render(); + }); + + document.body.addEventListener('mouseout', function () { + scene.traverse(function (child) { + if (child.isMesh) child.material.color.setHex(0xccccff); // tinted for visibility + }); + + render(); + }); + + // scene + + scene = new THREE.Scene(); + + // camera + camera = new THREE.PerspectiveCamera(40, aspect, 1, 30); + camera.position.set(0, 0, 18); +} + +function createObjects() { + const geometry = new THREE.SphereGeometry(0.4, 32, 16); + + for (let x = 0; x <= 10; x++) { + for (let y = 0; y <= 10; y++) { + const material = new THREE.MeshPhysicalMaterial({ + roughness: x / 10, + metalness: y / 10, + color: 0xffffff, + envMap: radianceMap, + envMapIntensity: 1, + transmission: 0, + ior: 1.5, + }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.x = x - 5; + mesh.position.y = 5 - y; + scene.add(mesh); + } + } +} + +function createEnvironment() { + const envScene = new THREE.Scene(); + envScene.background = new THREE.Color(COLOR); + + const pmremGenerator = new THREE.PMREMGenerator(renderer); + radianceMap = pmremGenerator.fromScene(envScene).texture; + pmremGenerator.dispose(); + + scene.background = envScene.background; +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + + render(); +} + +function render() { + renderer.render(scene, camera); +} + +Promise.resolve().then(init).then(createEnvironment).then(createObjects).then(render); diff --git a/examples-testing/examples/webgl_geometries.ts b/examples-testing/examples/webgl_geometries.ts new file mode 100644 index 000000000..2b2d02613 --- /dev/null +++ b/examples-testing/examples/webgl_geometries.ts @@ -0,0 +1,137 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let camera, scene, renderer, stats; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000); + camera.position.y = 400; + + scene = new THREE.Scene(); + + let object; + + const ambientLight = new THREE.AmbientLight(0xcccccc, 1.5); + scene.add(ambientLight); + + const pointLight = new THREE.PointLight(0xffffff, 2.5, 0, 0); + camera.add(pointLight); + scene.add(camera); + + const map = new THREE.TextureLoader().load('textures/uv_grid_opengl.jpg'); + map.wrapS = map.wrapT = THREE.RepeatWrapping; + map.anisotropy = 16; + map.colorSpace = THREE.SRGBColorSpace; + + const material = new THREE.MeshPhongMaterial({ map: map, side: THREE.DoubleSide }); + + // + + object = new THREE.Mesh(new THREE.SphereGeometry(75, 20, 10), material); + object.position.set(-300, 0, 200); + scene.add(object); + + object = new THREE.Mesh(new THREE.IcosahedronGeometry(75, 1), material); + object.position.set(-100, 0, 200); + scene.add(object); + + object = new THREE.Mesh(new THREE.OctahedronGeometry(75, 2), material); + object.position.set(100, 0, 200); + scene.add(object); + + object = new THREE.Mesh(new THREE.TetrahedronGeometry(75, 0), material); + object.position.set(300, 0, 200); + scene.add(object); + + // + + object = new THREE.Mesh(new THREE.PlaneGeometry(100, 100, 4, 4), material); + object.position.set(-300, 0, 0); + scene.add(object); + + object = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 100, 4, 4, 4), material); + object.position.set(-100, 0, 0); + scene.add(object); + + object = new THREE.Mesh(new THREE.CircleGeometry(50, 20, 0, Math.PI * 2), material); + object.position.set(100, 0, 0); + scene.add(object); + + object = new THREE.Mesh(new THREE.RingGeometry(10, 50, 20, 5, 0, Math.PI * 2), material); + object.position.set(300, 0, 0); + scene.add(object); + + // + + object = new THREE.Mesh(new THREE.CylinderGeometry(25, 75, 100, 40, 5), material); + object.position.set(-300, 0, -200); + scene.add(object); + + const points = []; + + for (let i = 0; i < 50; i++) { + points.push(new THREE.Vector2(Math.sin(i * 0.2) * Math.sin(i * 0.1) * 15 + 50, (i - 5) * 2)); + } + + object = new THREE.Mesh(new THREE.LatheGeometry(points, 20), material); + object.position.set(-100, 0, -200); + scene.add(object); + + object = new THREE.Mesh(new THREE.TorusGeometry(50, 20, 20, 20), material); + object.position.set(100, 0, -200); + scene.add(object); + + object = new THREE.Mesh(new THREE.TorusKnotGeometry(50, 10, 50, 20), material); + object.position.set(300, 0, -200); + scene.add(object); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + const timer = Date.now() * 0.0001; + + camera.position.x = Math.cos(timer) * 800; + camera.position.z = Math.sin(timer) * 800; + + camera.lookAt(scene.position); + + scene.traverse(function (object) { + if (object.isMesh === true) { + object.rotation.x = timer * 5; + object.rotation.y = timer * 2.5; + } + }); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_geometries_parametric.ts b/examples-testing/examples/webgl_geometries_parametric.ts new file mode 100644 index 000000000..29bf7ae26 --- /dev/null +++ b/examples-testing/examples/webgl_geometries_parametric.ts @@ -0,0 +1,124 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import * as Curves from 'three/addons/curves/CurveExtras.js'; +import { ParametricGeometry } from 'three/addons/geometries/ParametricGeometry.js'; +import { ParametricGeometries } from 'three/addons/geometries/ParametricGeometries.js'; + +let camera, scene, renderer, stats; + +init(); + +function init() { + const container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000); + camera.position.y = 400; + + scene = new THREE.Scene(); + + // + + const ambientLight = new THREE.AmbientLight(0xcccccc, 1.5); + scene.add(ambientLight); + + const pointLight = new THREE.PointLight(0xffffff, 2.5, 0, 0); + camera.add(pointLight); + scene.add(camera); + + // + + const map = new THREE.TextureLoader().load('textures/uv_grid_opengl.jpg'); + map.wrapS = map.wrapT = THREE.RepeatWrapping; + map.anisotropy = 16; + map.colorSpace = THREE.SRGBColorSpace; + + const material = new THREE.MeshPhongMaterial({ map: map, side: THREE.DoubleSide }); + + // + + let geometry, object; + + geometry = new ParametricGeometry(ParametricGeometries.plane(100, 100), 10, 10); + geometry.center(); + object = new THREE.Mesh(geometry, material); + object.position.set(-200, 0, 200); + scene.add(object); + + geometry = new ParametricGeometry(ParametricGeometries.klein, 20, 20); + object = new THREE.Mesh(geometry, material); + object.position.set(0, 0, 200); + object.scale.multiplyScalar(5); + scene.add(object); + + geometry = new ParametricGeometry(ParametricGeometries.mobius, 20, 20); + object = new THREE.Mesh(geometry, material); + object.position.set(200, 0, 200); + object.scale.multiplyScalar(30); + scene.add(object); + + // + + const GrannyKnot = new Curves.GrannyKnot(); + + const torus = new ParametricGeometries.TorusKnotGeometry(50, 10, 50, 20, 2, 3); + const sphere = new ParametricGeometries.SphereGeometry(50, 20, 10); + const tube = new ParametricGeometries.TubeGeometry(GrannyKnot, 100, 3, 8, true); + + object = new THREE.Mesh(torus, material); + object.position.set(-200, 0, -200); + scene.add(object); + + object = new THREE.Mesh(sphere, material); + object.position.set(0, 0, -200); + scene.add(object); + + object = new THREE.Mesh(tube, material); + object.position.set(200, 0, -200); + object.scale.multiplyScalar(2); + scene.add(object); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + const timer = Date.now() * 0.0001; + + camera.position.x = Math.cos(timer) * 800; + camera.position.z = Math.sin(timer) * 800; + + camera.lookAt(scene.position); + + scene.traverse(function (object) { + if (object.isMesh === true) { + object.rotation.x = timer * 5; + object.rotation.y = timer * 2.5; + } + }); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_geometry_colors.ts b/examples-testing/examples/webgl_geometry_colors.ts new file mode 100644 index 000000000..bc0bf5174 --- /dev/null +++ b/examples-testing/examples/webgl_geometry_colors.ts @@ -0,0 +1,176 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; + +let camera, scene, renderer; + +let mouseX = 0, + mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +init(); + +function init() { + container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(20, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 1800; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0, 0, 1); + scene.add(light); + + // shadow + + const canvas = document.createElement('canvas'); + canvas.width = 128; + canvas.height = 128; + + const context = canvas.getContext('2d'); + const gradient = context.createRadialGradient( + canvas.width / 2, + canvas.height / 2, + 0, + canvas.width / 2, + canvas.height / 2, + canvas.width / 2, + ); + gradient.addColorStop(0.1, 'rgba(210,210,210,1)'); + gradient.addColorStop(1, 'rgba(255,255,255,1)'); + + context.fillStyle = gradient; + context.fillRect(0, 0, canvas.width, canvas.height); + + const shadowTexture = new THREE.CanvasTexture(canvas); + + const shadowMaterial = new THREE.MeshBasicMaterial({ map: shadowTexture }); + const shadowGeo = new THREE.PlaneGeometry(300, 300, 1, 1); + + let shadowMesh; + + shadowMesh = new THREE.Mesh(shadowGeo, shadowMaterial); + shadowMesh.position.y = -250; + shadowMesh.rotation.x = -Math.PI / 2; + scene.add(shadowMesh); + + shadowMesh = new THREE.Mesh(shadowGeo, shadowMaterial); + shadowMesh.position.y = -250; + shadowMesh.position.x = -400; + shadowMesh.rotation.x = -Math.PI / 2; + scene.add(shadowMesh); + + shadowMesh = new THREE.Mesh(shadowGeo, shadowMaterial); + shadowMesh.position.y = -250; + shadowMesh.position.x = 400; + shadowMesh.rotation.x = -Math.PI / 2; + scene.add(shadowMesh); + + const radius = 200; + + const geometry1 = new THREE.IcosahedronGeometry(radius, 1); + + const count = geometry1.attributes.position.count; + geometry1.setAttribute('color', new THREE.BufferAttribute(new Float32Array(count * 3), 3)); + + const geometry2 = geometry1.clone(); + const geometry3 = geometry1.clone(); + + const color = new THREE.Color(); + const positions1 = geometry1.attributes.position; + const positions2 = geometry2.attributes.position; + const positions3 = geometry3.attributes.position; + const colors1 = geometry1.attributes.color; + const colors2 = geometry2.attributes.color; + const colors3 = geometry3.attributes.color; + + for (let i = 0; i < count; i++) { + color.setHSL((positions1.getY(i) / radius + 1) / 2, 1.0, 0.5, THREE.SRGBColorSpace); + colors1.setXYZ(i, color.r, color.g, color.b); + + color.setHSL(0, (positions2.getY(i) / radius + 1) / 2, 0.5, THREE.SRGBColorSpace); + colors2.setXYZ(i, color.r, color.g, color.b); + + color.setRGB(1, 0.8 - (positions3.getY(i) / radius + 1) / 2, 0, THREE.SRGBColorSpace); + colors3.setXYZ(i, color.r, color.g, color.b); + } + + const material = new THREE.MeshPhongMaterial({ + color: 0xffffff, + flatShading: true, + vertexColors: true, + shininess: 0, + }); + + const wireframeMaterial = new THREE.MeshBasicMaterial({ color: 0x000000, wireframe: true, transparent: true }); + + let mesh = new THREE.Mesh(geometry1, material); + let wireframe = new THREE.Mesh(geometry1, wireframeMaterial); + mesh.add(wireframe); + mesh.position.x = -400; + mesh.rotation.x = -1.87; + scene.add(mesh); + + mesh = new THREE.Mesh(geometry2, material); + wireframe = new THREE.Mesh(geometry2, wireframeMaterial); + mesh.add(wireframe); + mesh.position.x = 400; + scene.add(mesh); + + mesh = new THREE.Mesh(geometry3, material); + wireframe = new THREE.Mesh(geometry3, wireframeMaterial); + mesh.add(wireframe); + scene.add(mesh); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + document.addEventListener('mousemove', onDocumentMouseMove); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onDocumentMouseMove(event) { + mouseX = event.clientX - windowHalfX; + mouseY = event.clientY - windowHalfY; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-mouseY - camera.position.y) * 0.05; + + camera.lookAt(scene.position); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_geometry_colors_lookuptable.ts b/examples-testing/examples/webgl_geometry_colors_lookuptable.ts new file mode 100644 index 000000000..6b0138529 --- /dev/null +++ b/examples-testing/examples/webgl_geometry_colors_lookuptable.ts @@ -0,0 +1,148 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { Lut } from 'three/addons/math/Lut.js'; + +let container; + +let perpCamera, orthoCamera, renderer, lut; + +let mesh, sprite; +let scene, uiScene; + +let params; + +init(); + +function init() { + container = document.getElementById('container'); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + + uiScene = new THREE.Scene(); + + lut = new Lut(); + + const width = window.innerWidth; + const height = window.innerHeight; + + perpCamera = new THREE.PerspectiveCamera(60, width / height, 1, 100); + perpCamera.position.set(0, 0, 10); + scene.add(perpCamera); + + orthoCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 1, 2); + orthoCamera.position.set(0.5, 0, 1); + + sprite = new THREE.Sprite( + new THREE.SpriteMaterial({ + map: new THREE.CanvasTexture(lut.createCanvas()), + }), + ); + sprite.material.map.colorSpace = THREE.SRGBColorSpace; + sprite.scale.x = 0.125; + uiScene.add(sprite); + + mesh = new THREE.Mesh( + undefined, + new THREE.MeshLambertMaterial({ + side: THREE.DoubleSide, + color: 0xf5f5f5, + vertexColors: true, + }), + ); + scene.add(mesh); + + params = { + colorMap: 'rainbow', + }; + loadModel(); + + const pointLight = new THREE.PointLight(0xffffff, 3, 0, 0); + perpCamera.add(pointLight); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.autoClear = false; + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(width, height); + container.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); + + const controls = new OrbitControls(perpCamera, renderer.domElement); + controls.addEventListener('change', render); + + const gui = new GUI(); + + gui.add(params, 'colorMap', ['rainbow', 'cooltowarm', 'blackbody', 'grayscale']).onChange(function () { + updateColors(); + render(); + }); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + perpCamera.aspect = width / height; + perpCamera.updateProjectionMatrix(); + + renderer.setSize(width, height); + render(); +} + +function render() { + renderer.clear(); + renderer.render(scene, perpCamera); + renderer.render(uiScene, orthoCamera); +} + +function loadModel() { + const loader = new THREE.BufferGeometryLoader(); + loader.load('models/json/pressure.json', function (geometry) { + geometry.center(); + geometry.computeVertexNormals(); + + // default color attribute + const colors = []; + + for (let i = 0, n = geometry.attributes.position.count; i < n; ++i) { + colors.push(1, 1, 1); + } + + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + + mesh.geometry = geometry; + updateColors(); + + render(); + }); +} + +function updateColors() { + lut.setColorMap(params.colorMap); + + lut.setMax(2000); + lut.setMin(0); + + const geometry = mesh.geometry; + const pressures = geometry.attributes.pressure; + const colors = geometry.attributes.color; + const color = new THREE.Color(); + + for (let i = 0; i < pressures.array.length; i++) { + const colorValue = pressures.array[i]; + + color.copy(lut.getColor(colorValue)).convertSRGBToLinear(); + + colors.setXYZ(i, color.r, color.g, color.b); + } + + colors.needsUpdate = true; + + const map = sprite.material.map; + lut.updateCanvas(map.image); + map.needsUpdate = true; +} diff --git a/examples-testing/examples/webgl_geometry_convex.ts b/examples-testing/examples/webgl_geometry_convex.ts new file mode 100644 index 000000000..ade9cb801 --- /dev/null +++ b/examples-testing/examples/webgl_geometry_convex.ts @@ -0,0 +1,117 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { ConvexGeometry } from 'three/addons/geometries/ConvexGeometry.js'; +import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; + +let group, camera, scene, renderer; + +init(); + +function init() { + scene = new THREE.Scene(); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // camera + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(15, 20, 30); + scene.add(camera); + + // controls + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 20; + controls.maxDistance = 50; + controls.maxPolarAngle = Math.PI / 2; + + // ambient light + + scene.add(new THREE.AmbientLight(0x666666)); + + // point light + + const light = new THREE.PointLight(0xffffff, 3, 0, 0); + camera.add(light); + + // helper + + scene.add(new THREE.AxesHelper(20)); + + // textures + + const loader = new THREE.TextureLoader(); + const texture = loader.load('textures/sprites/disc.png'); + texture.colorSpace = THREE.SRGBColorSpace; + + group = new THREE.Group(); + scene.add(group); + + // points + + let dodecahedronGeometry = new THREE.DodecahedronGeometry(10); + + // if normal and uv attributes are not removed, mergeVertices() can't consolidate indentical vertices with different normal/uv data + + dodecahedronGeometry.deleteAttribute('normal'); + dodecahedronGeometry.deleteAttribute('uv'); + + dodecahedronGeometry = BufferGeometryUtils.mergeVertices(dodecahedronGeometry); + + const vertices = []; + const positionAttribute = dodecahedronGeometry.getAttribute('position'); + + for (let i = 0; i < positionAttribute.count; i++) { + const vertex = new THREE.Vector3(); + vertex.fromBufferAttribute(positionAttribute, i); + vertices.push(vertex); + } + + const pointsMaterial = new THREE.PointsMaterial({ + color: 0x0080ff, + map: texture, + size: 1, + alphaTest: 0.5, + }); + + const pointsGeometry = new THREE.BufferGeometry().setFromPoints(vertices); + + const points = new THREE.Points(pointsGeometry, pointsMaterial); + group.add(points); + + // convex hull + + const meshMaterial = new THREE.MeshLambertMaterial({ + color: 0xffffff, + opacity: 0.5, + side: THREE.DoubleSide, + transparent: true, + }); + + const meshGeometry = new ConvexGeometry(vertices); + + const mesh = new THREE.Mesh(meshGeometry, meshMaterial); + group.add(mesh); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + group.rotation.y += 0.005; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_geometry_cube.ts b/examples-testing/examples/webgl_geometry_cube.ts new file mode 100644 index 000000000..572601acb --- /dev/null +++ b/examples-testing/examples/webgl_geometry_cube.ts @@ -0,0 +1,46 @@ +import * as THREE from 'three'; + +let camera, scene, renderer; +let mesh; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.z = 2; + + scene = new THREE.Scene(); + + const texture = new THREE.TextureLoader().load('textures/crate.gif'); + texture.colorSpace = THREE.SRGBColorSpace; + + const geometry = new THREE.BoxGeometry(); + const material = new THREE.MeshBasicMaterial({ map: texture }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + mesh.rotation.x += 0.005; + mesh.rotation.y += 0.01; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_geometry_dynamic.ts b/examples-testing/examples/webgl_geometry_dynamic.ts new file mode 100644 index 000000000..06e858f54 --- /dev/null +++ b/examples-testing/examples/webgl_geometry_dynamic.ts @@ -0,0 +1,97 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js'; + +let camera, controls, scene, renderer, stats; + +let mesh, geometry, material, clock; + +const worldWidth = 128, + worldDepth = 128; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 20000); + camera.position.y = 200; + + clock = new THREE.Clock(); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xaaccff); + scene.fog = new THREE.FogExp2(0xaaccff, 0.0007); + + geometry = new THREE.PlaneGeometry(20000, 20000, worldWidth - 1, worldDepth - 1); + geometry.rotateX(-Math.PI / 2); + + const position = geometry.attributes.position; + position.usage = THREE.DynamicDrawUsage; + + for (let i = 0; i < position.count; i++) { + const y = 35 * Math.sin(i / 2); + position.setY(i, y); + } + + const texture = new THREE.TextureLoader().load('textures/water.jpg'); + texture.wrapS = texture.wrapT = THREE.RepeatWrapping; + texture.repeat.set(5, 5); + texture.colorSpace = THREE.SRGBColorSpace; + + material = new THREE.MeshBasicMaterial({ color: 0x0044ff, map: texture }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + controls = new FirstPersonControls(camera, renderer.domElement); + + controls.movementSpeed = 500; + controls.lookSpeed = 0.1; + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + controls.handleResize(); +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + const delta = clock.getDelta(); + const time = clock.getElapsedTime() * 10; + + const position = geometry.attributes.position; + + for (let i = 0; i < position.count; i++) { + const y = 35 * Math.sin(i / 5 + (time + i) / 7); + position.setY(i, y); + } + + position.needsUpdate = true; + + controls.update(delta); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_geometry_extrude_shapes.ts b/examples-testing/examples/webgl_geometry_extrude_shapes.ts new file mode 100644 index 000000000..7428aee31 --- /dev/null +++ b/examples-testing/examples/webgl_geometry_extrude_shapes.ts @@ -0,0 +1,149 @@ +import * as THREE from 'three'; + +import { TrackballControls } from 'three/addons/controls/TrackballControls.js'; + +let camera, scene, renderer, controls; + +init(); + +function init() { + const info = document.createElement('div'); + info.style.position = 'absolute'; + info.style.top = '10px'; + info.style.width = '100%'; + info.style.textAlign = 'center'; + info.style.color = '#fff'; + info.style.link = '#f80'; + info.innerHTML = + 'three.js webgl - geometry extrude shapes'; + document.body.appendChild(info); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x222222); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 0, 500); + + controls = new TrackballControls(camera, renderer.domElement); + controls.minDistance = 200; + controls.maxDistance = 500; + + scene.add(new THREE.AmbientLight(0x666666)); + + const light = new THREE.PointLight(0xffffff, 3, 0, 0); + light.position.copy(camera.position); + scene.add(light); + + // + + const closedSpline = new THREE.CatmullRomCurve3([ + new THREE.Vector3(-60, -100, 60), + new THREE.Vector3(-60, 20, 60), + new THREE.Vector3(-60, 120, 60), + new THREE.Vector3(60, 20, -60), + new THREE.Vector3(60, -100, -60), + ]); + + closedSpline.curveType = 'catmullrom'; + closedSpline.closed = true; + + const extrudeSettings1 = { + steps: 100, + bevelEnabled: false, + extrudePath: closedSpline, + }; + + const pts1 = [], + count = 3; + + for (let i = 0; i < count; i++) { + const l = 20; + + const a = ((2 * i) / count) * Math.PI; + + pts1.push(new THREE.Vector2(Math.cos(a) * l, Math.sin(a) * l)); + } + + const shape1 = new THREE.Shape(pts1); + + const geometry1 = new THREE.ExtrudeGeometry(shape1, extrudeSettings1); + + const material1 = new THREE.MeshLambertMaterial({ color: 0xb00000, wireframe: false }); + + const mesh1 = new THREE.Mesh(geometry1, material1); + + scene.add(mesh1); + + // + + const randomPoints = []; + + for (let i = 0; i < 10; i++) { + randomPoints.push( + new THREE.Vector3((i - 4.5) * 50, THREE.MathUtils.randFloat(-50, 50), THREE.MathUtils.randFloat(-50, 50)), + ); + } + + const randomSpline = new THREE.CatmullRomCurve3(randomPoints); + + // + + const extrudeSettings2 = { + steps: 200, + bevelEnabled: false, + extrudePath: randomSpline, + }; + + const pts2 = [], + numPts = 5; + + for (let i = 0; i < numPts * 2; i++) { + const l = i % 2 == 1 ? 10 : 20; + + const a = (i / numPts) * Math.PI; + + pts2.push(new THREE.Vector2(Math.cos(a) * l, Math.sin(a) * l)); + } + + const shape2 = new THREE.Shape(pts2); + + const geometry2 = new THREE.ExtrudeGeometry(shape2, extrudeSettings2); + + const material2 = new THREE.MeshLambertMaterial({ color: 0xff8000, wireframe: false }); + + const mesh2 = new THREE.Mesh(geometry2, material2); + + scene.add(mesh2); + + // + + const materials = [material1, material2]; + + const extrudeSettings3 = { + depth: 20, + steps: 1, + bevelEnabled: true, + bevelThickness: 2, + bevelSize: 4, + bevelSegments: 1, + }; + + const geometry3 = new THREE.ExtrudeGeometry(shape2, extrudeSettings3); + + const mesh3 = new THREE.Mesh(geometry3, materials); + + mesh3.position.set(50, 100, 50); + + scene.add(mesh3); +} + +function animate() { + controls.update(); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_geometry_extrude_splines.ts b/examples-testing/examples/webgl_geometry_extrude_splines.ts new file mode 100644 index 000000000..0741083da --- /dev/null +++ b/examples-testing/examples/webgl_geometry_extrude_splines.ts @@ -0,0 +1,310 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import * as Curves from 'three/addons/curves/CurveExtras.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let container, stats; + +let camera, scene, renderer, splineCamera, cameraHelper, cameraEye; + +const direction = new THREE.Vector3(); +const binormal = new THREE.Vector3(); +const normal = new THREE.Vector3(); +const position = new THREE.Vector3(); +const lookAt = new THREE.Vector3(); + +const pipeSpline = new THREE.CatmullRomCurve3([ + new THREE.Vector3(0, 10, -10), + new THREE.Vector3(10, 0, -10), + new THREE.Vector3(20, 0, 0), + new THREE.Vector3(30, 0, 10), + new THREE.Vector3(30, 0, 20), + new THREE.Vector3(20, 0, 30), + new THREE.Vector3(10, 0, 30), + new THREE.Vector3(0, 0, 30), + new THREE.Vector3(-10, 10, 30), + new THREE.Vector3(-10, 20, 30), + new THREE.Vector3(0, 30, 30), + new THREE.Vector3(10, 30, 30), + new THREE.Vector3(20, 30, 15), + new THREE.Vector3(10, 30, 10), + new THREE.Vector3(0, 30, 10), + new THREE.Vector3(-10, 20, 10), + new THREE.Vector3(-10, 10, 10), + new THREE.Vector3(0, 0, 10), + new THREE.Vector3(10, -10, 10), + new THREE.Vector3(20, -15, 10), + new THREE.Vector3(30, -15, 10), + new THREE.Vector3(40, -15, 10), + new THREE.Vector3(50, -15, 10), + new THREE.Vector3(60, 0, 10), + new THREE.Vector3(70, 0, 0), + new THREE.Vector3(80, 0, 0), + new THREE.Vector3(90, 0, 0), + new THREE.Vector3(100, 0, 0), +]); + +const sampleClosedSpline = new THREE.CatmullRomCurve3([ + new THREE.Vector3(0, -40, -40), + new THREE.Vector3(0, 40, -40), + new THREE.Vector3(0, 140, -40), + new THREE.Vector3(0, 40, 40), + new THREE.Vector3(0, -40, 40), +]); + +sampleClosedSpline.curveType = 'catmullrom'; +sampleClosedSpline.closed = true; + +// Keep a dictionary of Curve instances +const splines = { + GrannyKnot: new Curves.GrannyKnot(), + HeartCurve: new Curves.HeartCurve(3.5), + VivianiCurve: new Curves.VivianiCurve(70), + KnotCurve: new Curves.KnotCurve(), + HelixCurve: new Curves.HelixCurve(), + TrefoilKnot: new Curves.TrefoilKnot(), + TorusKnot: new Curves.TorusKnot(20), + CinquefoilKnot: new Curves.CinquefoilKnot(20), + TrefoilPolynomialKnot: new Curves.TrefoilPolynomialKnot(14), + FigureEightPolynomialKnot: new Curves.FigureEightPolynomialKnot(), + DecoratedTorusKnot4a: new Curves.DecoratedTorusKnot4a(), + DecoratedTorusKnot4b: new Curves.DecoratedTorusKnot4b(), + DecoratedTorusKnot5a: new Curves.DecoratedTorusKnot5a(), + DecoratedTorusKnot5c: new Curves.DecoratedTorusKnot5c(), + PipeSpline: pipeSpline, + SampleClosedSpline: sampleClosedSpline, +}; + +let parent, tubeGeometry, mesh; + +const params = { + spline: 'GrannyKnot', + scale: 4, + extrusionSegments: 100, + radiusSegments: 3, + closed: true, + animationView: false, + lookAhead: false, + cameraHelper: false, +}; + +const material = new THREE.MeshLambertMaterial({ color: 0xff00ff }); + +const wireframeMaterial = new THREE.MeshBasicMaterial({ + color: 0x000000, + opacity: 0.3, + wireframe: true, + transparent: true, +}); + +function addTube() { + if (mesh !== undefined) { + parent.remove(mesh); + mesh.geometry.dispose(); + } + + const extrudePath = splines[params.spline]; + + tubeGeometry = new THREE.TubeGeometry( + extrudePath, + params.extrusionSegments, + 2, + params.radiusSegments, + params.closed, + ); + + addGeometry(tubeGeometry); + + setScale(); +} + +function setScale() { + mesh.scale.set(params.scale, params.scale, params.scale); +} + +function addGeometry(geometry) { + // 3D shape + + mesh = new THREE.Mesh(geometry, material); + const wireframe = new THREE.Mesh(geometry, wireframeMaterial); + mesh.add(wireframe); + + parent.add(mesh); +} + +function animateCamera() { + cameraHelper.visible = params.cameraHelper; + cameraEye.visible = params.cameraHelper; +} + +init(); + +function init() { + container = document.getElementById('container'); + + // camera + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.01, 10000); + camera.position.set(0, 50, 500); + + // scene + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + // light + + scene.add(new THREE.AmbientLight(0xffffff)); + + const light = new THREE.DirectionalLight(0xffffff, 1.5); + light.position.set(0, 0, 1); + scene.add(light); + + // tube + + parent = new THREE.Object3D(); + scene.add(parent); + + splineCamera = new THREE.PerspectiveCamera(84, window.innerWidth / window.innerHeight, 0.01, 1000); + parent.add(splineCamera); + + cameraHelper = new THREE.CameraHelper(splineCamera); + scene.add(cameraHelper); + + addTube(); + + // debug camera + + cameraEye = new THREE.Mesh(new THREE.SphereGeometry(5), new THREE.MeshBasicMaterial({ color: 0xdddddd })); + parent.add(cameraEye); + + cameraHelper.visible = params.cameraHelper; + cameraEye.visible = params.cameraHelper; + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // stats + + stats = new Stats(); + container.appendChild(stats.dom); + + // dat.GUI + + const gui = new GUI({ width: 285 }); + + const folderGeometry = gui.addFolder('Geometry'); + folderGeometry.add(params, 'spline', Object.keys(splines)).onChange(function () { + addTube(); + }); + folderGeometry + .add(params, 'scale', 2, 10) + .step(2) + .onChange(function () { + setScale(); + }); + folderGeometry + .add(params, 'extrusionSegments', 50, 500) + .step(50) + .onChange(function () { + addTube(); + }); + folderGeometry + .add(params, 'radiusSegments', 2, 12) + .step(1) + .onChange(function () { + addTube(); + }); + folderGeometry.add(params, 'closed').onChange(function () { + addTube(); + }); + folderGeometry.open(); + + const folderCamera = gui.addFolder('Camera'); + folderCamera.add(params, 'animationView').onChange(function () { + animateCamera(); + }); + folderCamera.add(params, 'lookAhead').onChange(function () { + animateCamera(); + }); + folderCamera.add(params, 'cameraHelper').onChange(function () { + animateCamera(); + }); + folderCamera.open(); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 100; + controls.maxDistance = 2000; + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + // animate camera along spline + + const time = Date.now(); + const looptime = 20 * 1000; + const t = (time % looptime) / looptime; + + tubeGeometry.parameters.path.getPointAt(t, position); + position.multiplyScalar(params.scale); + + // interpolation + + const segments = tubeGeometry.tangents.length; + const pickt = t * segments; + const pick = Math.floor(pickt); + const pickNext = (pick + 1) % segments; + + binormal.subVectors(tubeGeometry.binormals[pickNext], tubeGeometry.binormals[pick]); + binormal.multiplyScalar(pickt - pick).add(tubeGeometry.binormals[pick]); + + tubeGeometry.parameters.path.getTangentAt(t, direction); + const offset = 15; + + normal.copy(binormal).cross(direction); + + // we move on a offset on its binormal + + position.add(normal.clone().multiplyScalar(offset)); + + splineCamera.position.copy(position); + cameraEye.position.copy(position); + + // using arclength for stablization in look ahead + + tubeGeometry.parameters.path.getPointAt((t + 30 / tubeGeometry.parameters.path.getLength()) % 1, lookAt); + lookAt.multiplyScalar(params.scale); + + // camera orientation 2 - up orientation via normal + + if (!params.lookAhead) lookAt.copy(position).add(direction); + splineCamera.matrix.lookAt(splineCamera.position, lookAt, normal); + splineCamera.quaternion.setFromRotationMatrix(splineCamera.matrix); + + cameraHelper.update(); + + renderer.render(scene, params.animationView === true ? splineCamera : camera); +} diff --git a/examples-testing/examples/webgl_geometry_minecraft.ts b/examples-testing/examples/webgl_geometry_minecraft.ts new file mode 100644 index 000000000..765aa1e49 --- /dev/null +++ b/examples-testing/examples/webgl_geometry_minecraft.ts @@ -0,0 +1,183 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js'; +import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js'; +import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; + +let container, stats; + +let camera, controls, scene, renderer; + +const worldWidth = 128, + worldDepth = 128; +const worldHalfWidth = worldWidth / 2; +const worldHalfDepth = worldDepth / 2; +const data = generateHeight(worldWidth, worldDepth); + +const clock = new THREE.Clock(); + +init(); + +function init() { + container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 20000); + camera.position.y = getY(worldHalfWidth, worldHalfDepth) * 100 + 100; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xbfd1e5); + + // sides + + const matrix = new THREE.Matrix4(); + + const pxGeometry = new THREE.PlaneGeometry(100, 100); + pxGeometry.attributes.uv.array[1] = 0.5; + pxGeometry.attributes.uv.array[3] = 0.5; + pxGeometry.rotateY(Math.PI / 2); + pxGeometry.translate(50, 0, 0); + + const nxGeometry = new THREE.PlaneGeometry(100, 100); + nxGeometry.attributes.uv.array[1] = 0.5; + nxGeometry.attributes.uv.array[3] = 0.5; + nxGeometry.rotateY(-Math.PI / 2); + nxGeometry.translate(-50, 0, 0); + + const pyGeometry = new THREE.PlaneGeometry(100, 100); + pyGeometry.attributes.uv.array[5] = 0.5; + pyGeometry.attributes.uv.array[7] = 0.5; + pyGeometry.rotateX(-Math.PI / 2); + pyGeometry.translate(0, 50, 0); + + const pzGeometry = new THREE.PlaneGeometry(100, 100); + pzGeometry.attributes.uv.array[1] = 0.5; + pzGeometry.attributes.uv.array[3] = 0.5; + pzGeometry.translate(0, 0, 50); + + const nzGeometry = new THREE.PlaneGeometry(100, 100); + nzGeometry.attributes.uv.array[1] = 0.5; + nzGeometry.attributes.uv.array[3] = 0.5; + nzGeometry.rotateY(Math.PI); + nzGeometry.translate(0, 0, -50); + + // + + const geometries = []; + + for (let z = 0; z < worldDepth; z++) { + for (let x = 0; x < worldWidth; x++) { + const h = getY(x, z); + + matrix.makeTranslation(x * 100 - worldHalfWidth * 100, h * 100, z * 100 - worldHalfDepth * 100); + + const px = getY(x + 1, z); + const nx = getY(x - 1, z); + const pz = getY(x, z + 1); + const nz = getY(x, z - 1); + + geometries.push(pyGeometry.clone().applyMatrix4(matrix)); + + if ((px !== h && px !== h + 1) || x === 0) { + geometries.push(pxGeometry.clone().applyMatrix4(matrix)); + } + + if ((nx !== h && nx !== h + 1) || x === worldWidth - 1) { + geometries.push(nxGeometry.clone().applyMatrix4(matrix)); + } + + if ((pz !== h && pz !== h + 1) || z === worldDepth - 1) { + geometries.push(pzGeometry.clone().applyMatrix4(matrix)); + } + + if ((nz !== h && nz !== h + 1) || z === 0) { + geometries.push(nzGeometry.clone().applyMatrix4(matrix)); + } + } + } + + const geometry = BufferGeometryUtils.mergeGeometries(geometries); + geometry.computeBoundingSphere(); + + const texture = new THREE.TextureLoader().load('textures/minecraft/atlas.png'); + texture.colorSpace = THREE.SRGBColorSpace; + texture.magFilter = THREE.NearestFilter; + + const mesh = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ map: texture, side: THREE.DoubleSide })); + scene.add(mesh); + + const ambientLight = new THREE.AmbientLight(0xeeeeee, 3); + scene.add(ambientLight); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 12); + directionalLight.position.set(1, 1, 0.5).normalize(); + scene.add(directionalLight); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + controls = new FirstPersonControls(camera, renderer.domElement); + + controls.movementSpeed = 1000; + controls.lookSpeed = 0.125; + controls.lookVertical = true; + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + controls.handleResize(); +} + +function generateHeight(width, height) { + const data = [], + perlin = new ImprovedNoise(), + size = width * height, + z = Math.random() * 100; + + let quality = 2; + + for (let j = 0; j < 4; j++) { + if (j === 0) for (let i = 0; i < size; i++) data[i] = 0; + + for (let i = 0; i < size; i++) { + const x = i % width, + y = (i / width) | 0; + data[i] += perlin.noise(x / quality, y / quality, z) * quality; + } + + quality *= 4; + } + + return data; +} + +function getY(x, z) { + return (data[x + z * worldWidth] * 0.15) | 0; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + controls.update(clock.getDelta()); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_geometry_nurbs.ts b/examples-testing/examples/webgl_geometry_nurbs.ts new file mode 100644 index 000000000..a603710bd --- /dev/null +++ b/examples-testing/examples/webgl_geometry_nurbs.ts @@ -0,0 +1,298 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { NURBSCurve } from 'three/addons/curves/NURBSCurve.js'; +import { NURBSSurface } from 'three/addons/curves/NURBSSurface.js'; +import { NURBSVolume } from 'three/addons/curves/NURBSVolume.js'; +import { ParametricGeometry } from 'three/addons/geometries/ParametricGeometry.js'; + +let container, stats; + +let camera, scene, renderer; +let group; + +let targetRotation = 0; +let targetRotationOnPointerDown = 0; + +let pointerX = 0; +let pointerXOnPointerDown = 0; + +let windowHalfX = window.innerWidth / 2; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 2000); + camera.position.set(0, 150, 750); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + scene.add(new THREE.AmbientLight(0xffffff)); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(1, 1, 1); + scene.add(light); + + group = new THREE.Group(); + group.position.y = 50; + scene.add(group); + + // NURBS curve + + const nurbsControlPoints = []; + const nurbsKnots = []; + const nurbsDegree = 3; + + for (let i = 0; i <= nurbsDegree; i++) { + nurbsKnots.push(0); + } + + for (let i = 0, j = 20; i < j; i++) { + nurbsControlPoints.push( + new THREE.Vector4( + Math.random() * 400 - 200, + Math.random() * 400, + Math.random() * 400 - 200, + 1, // weight of control point: higher means stronger attraction + ), + ); + + const knot = (i + 1) / (j - nurbsDegree); + nurbsKnots.push(THREE.MathUtils.clamp(knot, 0, 1)); + } + + const nurbsCurve = new NURBSCurve(nurbsDegree, nurbsKnots, nurbsControlPoints); + + const nurbsGeometry = new THREE.BufferGeometry(); + nurbsGeometry.setFromPoints(nurbsCurve.getPoints(200)); + + const nurbsMaterial = new THREE.LineBasicMaterial({ color: 0x333333 }); + + const nurbsLine = new THREE.Line(nurbsGeometry, nurbsMaterial); + nurbsLine.position.set(0, -100, 0); + group.add(nurbsLine); + + const nurbsControlPointsGeometry = new THREE.BufferGeometry(); + nurbsControlPointsGeometry.setFromPoints(nurbsCurve.controlPoints); + + const nurbsControlPointsMaterial = new THREE.LineBasicMaterial({ + color: 0x333333, + opacity: 0.25, + transparent: true, + }); + + const nurbsControlPointsLine = new THREE.Line(nurbsControlPointsGeometry, nurbsControlPointsMaterial); + nurbsControlPointsLine.position.copy(nurbsLine.position); + group.add(nurbsControlPointsLine); + + // NURBS surface + { + const nsControlPoints = [ + [ + new THREE.Vector4(-200, -200, 100, 1), + new THREE.Vector4(-200, -100, -200, 1), + new THREE.Vector4(-200, 100, 250, 1), + new THREE.Vector4(-200, 200, -100, 1), + ], + [ + new THREE.Vector4(0, -200, 0, 1), + new THREE.Vector4(0, -100, -100, 5), + new THREE.Vector4(0, 100, 150, 5), + new THREE.Vector4(0, 200, 0, 1), + ], + [ + new THREE.Vector4(200, -200, -100, 1), + new THREE.Vector4(200, -100, 200, 1), + new THREE.Vector4(200, 100, -250, 1), + new THREE.Vector4(200, 200, 100, 1), + ], + ]; + const degree1 = 2; + const degree2 = 3; + const knots1 = [0, 0, 0, 1, 1, 1]; + const knots2 = [0, 0, 0, 0, 1, 1, 1, 1]; + const nurbsSurface = new NURBSSurface(degree1, degree2, knots1, knots2, nsControlPoints); + + const map = new THREE.TextureLoader().load('textures/uv_grid_opengl.jpg'); + map.wrapS = map.wrapT = THREE.RepeatWrapping; + map.anisotropy = 16; + map.colorSpace = THREE.SRGBColorSpace; + + function getSurfacePoint(u, v, target) { + return nurbsSurface.getPoint(u, v, target); + } + + const geometry = new ParametricGeometry(getSurfacePoint, 20, 20); + const material = new THREE.MeshLambertMaterial({ map: map, side: THREE.DoubleSide }); + const object = new THREE.Mesh(geometry, material); + object.position.set(-400, 100, 0); + object.scale.multiplyScalar(1); + group.add(object); + } + + // NURBS volume + { + const nsControlPoints = [ + [ + [new THREE.Vector4(-200, -200, -200, 1), new THREE.Vector4(-200, -200, 200, 1)], + [new THREE.Vector4(-200, -100, -200, 1), new THREE.Vector4(-200, -100, 200, 1)], + [new THREE.Vector4(-200, 100, -200, 1), new THREE.Vector4(-200, 100, 200, 1)], + [new THREE.Vector4(-200, 200, -200, 1), new THREE.Vector4(-200, 200, 200, 1)], + ], + [ + [new THREE.Vector4(0, -200, -200, 1), new THREE.Vector4(0, -200, 200, 1)], + [new THREE.Vector4(0, -100, -200, 1), new THREE.Vector4(0, -100, 200, 1)], + [new THREE.Vector4(0, 100, -200, 1), new THREE.Vector4(0, 100, 200, 1)], + [new THREE.Vector4(0, 200, -200, 1), new THREE.Vector4(0, 200, 200, 1)], + ], + [ + [new THREE.Vector4(200, -200, -200, 1), new THREE.Vector4(200, -200, 200, 1)], + [new THREE.Vector4(200, -100, 0, 1), new THREE.Vector4(200, -100, 100, 1)], + [new THREE.Vector4(200, 100, 0, 1), new THREE.Vector4(200, 100, 100, 1)], + [new THREE.Vector4(200, 200, 0, 1), new THREE.Vector4(200, 200, 100, 1)], + ], + ]; + const degree1 = 2; + const degree2 = 3; + const degree3 = 1; + const knots1 = [0, 0, 0, 1, 1, 1]; + const knots2 = [0, 0, 0, 0, 1, 1, 1, 1]; + const knots3 = [0, 0, 1, 1]; + const nurbsVolume = new NURBSVolume(degree1, degree2, degree3, knots1, knots2, knots3, nsControlPoints); + + const map = new THREE.TextureLoader().load('textures/uv_grid_opengl.jpg'); + map.wrapS = map.wrapT = THREE.RepeatWrapping; + map.anisotropy = 16; + map.colorSpace = THREE.SRGBColorSpace; + + // Since ParametricGeometry() only support bi-variate parametric geometries + // we create evaluation functions for different surfaces with one of the three + // parameter values (u, v, w) kept constant and create multiple THREE.Mesh + // objects one for each surface + function getSurfacePointFront(u, v, target) { + return nurbsVolume.getPoint(u, v, 0, target); + } + + function getSurfacePointMiddle(u, v, target) { + return nurbsVolume.getPoint(u, v, 0.5, target); + } + + function getSurfacePointBack(u, v, target) { + return nurbsVolume.getPoint(u, v, 1, target); + } + + function getSurfacePointTop(u, w, target) { + return nurbsVolume.getPoint(u, 1, w, target); + } + + function getSurfacePointSide(v, w, target) { + return nurbsVolume.getPoint(0, v, w, target); + } + + const geometryFront = new ParametricGeometry(getSurfacePointFront, 20, 20); + const materialFront = new THREE.MeshLambertMaterial({ map: map, side: THREE.DoubleSide }); + const objectFront = new THREE.Mesh(geometryFront, materialFront); + objectFront.position.set(400, 100, 0); + objectFront.scale.multiplyScalar(0.5); + group.add(objectFront); + + const geometryMiddle = new ParametricGeometry(getSurfacePointMiddle, 20, 20); + const materialMiddle = new THREE.MeshLambertMaterial({ map: map, side: THREE.DoubleSide }); + const objectMiddle = new THREE.Mesh(geometryMiddle, materialMiddle); + objectMiddle.position.set(400, 100, 0); + objectMiddle.scale.multiplyScalar(0.5); + group.add(objectMiddle); + + const geometryBack = new ParametricGeometry(getSurfacePointBack, 20, 20); + const materialBack = new THREE.MeshLambertMaterial({ map: map, side: THREE.DoubleSide }); + const objectBack = new THREE.Mesh(geometryBack, materialBack); + objectBack.position.set(400, 100, 0); + objectBack.scale.multiplyScalar(0.5); + group.add(objectBack); + + const geometryTop = new ParametricGeometry(getSurfacePointTop, 20, 20); + const materialTop = new THREE.MeshLambertMaterial({ map: map, side: THREE.DoubleSide }); + const objectTop = new THREE.Mesh(geometryTop, materialTop); + objectTop.position.set(400, 100, 0); + objectTop.scale.multiplyScalar(0.5); + group.add(objectTop); + + const geometrySide = new ParametricGeometry(getSurfacePointSide, 20, 20); + const materialSide = new THREE.MeshLambertMaterial({ map: map, side: THREE.DoubleSide }); + const objectSide = new THREE.Mesh(geometrySide, materialSide); + objectSide.position.set(400, 100, 0); + objectSide.scale.multiplyScalar(0.5); + group.add(objectSide); + } + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + container.style.touchAction = 'none'; + container.addEventListener('pointerdown', onPointerDown); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function onPointerDown(event) { + if (event.isPrimary === false) return; + + pointerXOnPointerDown = event.clientX - windowHalfX; + targetRotationOnPointerDown = targetRotation; + + document.addEventListener('pointermove', onPointerMove); + document.addEventListener('pointerup', onPointerUp); +} + +function onPointerMove(event) { + if (event.isPrimary === false) return; + + pointerX = event.clientX - windowHalfX; + + targetRotation = targetRotationOnPointerDown + (pointerX - pointerXOnPointerDown) * 0.02; +} + +function onPointerUp() { + if (event.isPrimary === false) return; + + document.removeEventListener('pointermove', onPointerMove); + document.removeEventListener('pointerup', onPointerUp); +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + group.rotation.y += (targetRotation - group.rotation.y) * 0.05; + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_geometry_sdf.ts b/examples-testing/examples/webgl_geometry_sdf.ts new file mode 100644 index 000000000..fa5dba102 --- /dev/null +++ b/examples-testing/examples/webgl_geometry_sdf.ts @@ -0,0 +1,164 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { SDFGeometryGenerator } from 'three/addons/geometries/SDFGeometryGenerator.js'; +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let renderer, stats, meshFromSDF, scene, camera, clock, controls; + +const settings = { + res: 4, + bounds: 1, + autoRotate: true, + wireframe: true, + material: 'depth', + vertexCount: '0', +}; + +// Example SDF from https://www.shadertoy.com/view/MdXSWn --> + +const shader = /* glsl */ ` + float dist(vec3 p) { + p.xyz = p.xzy; + p *= 1.2; + vec3 z = p; + vec3 dz=vec3(0.0); + float power = 8.0; + float r, theta, phi; + float dr = 1.0; + + float t0 = 1.0; + for(int i = 0; i < 7; ++i) { + r = length(z); + if(r > 2.0) continue; + theta = atan(z.y / z.x); + #ifdef phase_shift_on + phi = asin(z.z / r) ; + #else + phi = asin(z.z / r); + #endif + + dr = pow(r, power - 1.0) * dr * power + 1.0; + + r = pow(r, power); + theta = theta * power; + phi = phi * power; + + z = r * vec3(cos(theta)*cos(phi), sin(theta)*cos(phi), sin(phi)) + p; + + t0 = min(t0, r); + } + + return 0.5 * log(r) * r / dr; + } + `; + +init(); + +function init() { + const w = window.innerWidth; + const h = window.innerHeight; + + camera = new THREE.OrthographicCamera(w / -2, w / 2, h / 2, h / -2, 0.01, 1600); + camera.position.z = 1100; + + scene = new THREE.Scene(); + + clock = new THREE.Clock(); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + + window.addEventListener('resize', onWindowResize); + + // + + const panel = new GUI(); + + panel.add(settings, 'res', 1, 6, 1).name('Res').onFinishChange(compile); + panel.add(settings, 'bounds', 1, 10, 1).name('Bounds').onFinishChange(compile); + panel.add(settings, 'material', ['depth', 'normal']).name('Material').onChange(setMaterial); + panel.add(settings, 'wireframe').name('Wireframe').onChange(setMaterial); + panel.add(settings, 'autoRotate').name('Auto Rotate'); + panel.add(settings, 'vertexCount').name('Vertex count').listen().disable(); + + // + + compile(); +} + +function compile() { + const generator = new SDFGeometryGenerator(renderer); + const geometry = generator.generate(Math.pow(2, settings.res + 2), shader, settings.bounds); + geometry.computeVertexNormals(); + + if (meshFromSDF) { + // updates mesh + + meshFromSDF.geometry.dispose(); + meshFromSDF.geometry = geometry; + } else { + // inits meshFromSDF : THREE.Mesh + + meshFromSDF = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial()); + scene.add(meshFromSDF); + + const scale = (Math.min(window.innerWidth, window.innerHeight) / 2) * 0.66; + meshFromSDF.scale.set(scale, scale, scale); + + setMaterial(); + } + + settings.vertexCount = geometry.attributes.position.count; +} + +function setMaterial() { + meshFromSDF.material.dispose(); + + if (settings.material == 'depth') { + meshFromSDF.material = new THREE.MeshDepthMaterial(); + } else if (settings.material == 'normal') { + meshFromSDF.material = new THREE.MeshNormalMaterial(); + } + + meshFromSDF.material.wireframe = settings.wireframe; +} + +function onWindowResize() { + const w = window.innerWidth; + const h = window.innerHeight; + + renderer.setSize(w, h); + + camera.left = w / -2; + camera.right = w / 2; + camera.top = h / 2; + camera.bottom = h / -2; + + camera.updateProjectionMatrix(); +} + +function render() { + renderer.render(scene, camera); +} + +function animate() { + controls.update(); + + if (settings.autoRotate) { + meshFromSDF.rotation.y += Math.PI * 0.05 * clock.getDelta(); + } + + render(); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_geometry_shapes.ts b/examples-testing/examples/webgl_geometry_shapes.ts new file mode 100644 index 000000000..f1d00f011 --- /dev/null +++ b/examples-testing/examples/webgl_geometry_shapes.ts @@ -0,0 +1,363 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; + +let camera, scene, renderer; + +let group; + +let targetRotation = 0; +let targetRotationOnPointerDown = 0; + +let pointerX = 0; +let pointerXOnPointerDown = 0; + +let windowHalfX = window.innerWidth / 2; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 150, 500); + scene.add(camera); + + const light = new THREE.PointLight(0xffffff, 2.5, 0, 0); + camera.add(light); + + group = new THREE.Group(); + group.position.y = 50; + scene.add(group); + + const loader = new THREE.TextureLoader(); + const texture = loader.load('textures/uv_grid_opengl.jpg'); + texture.colorSpace = THREE.SRGBColorSpace; + + // it's necessary to apply these settings in order to correctly display the texture on a shape geometry + + texture.wrapS = texture.wrapT = THREE.RepeatWrapping; + texture.repeat.set(0.008, 0.008); + + function addShape(shape, extrudeSettings, color, x, y, z, rx, ry, rz, s) { + // flat shape with texture + // note: default UVs generated by THREE.ShapeGeometry are simply the x- and y-coordinates of the vertices + + let geometry = new THREE.ShapeGeometry(shape); + + let mesh = new THREE.Mesh(geometry, new THREE.MeshPhongMaterial({ side: THREE.DoubleSide, map: texture })); + mesh.position.set(x, y, z - 175); + mesh.rotation.set(rx, ry, rz); + mesh.scale.set(s, s, s); + group.add(mesh); + + // flat shape + + geometry = new THREE.ShapeGeometry(shape); + + mesh = new THREE.Mesh(geometry, new THREE.MeshPhongMaterial({ color: color, side: THREE.DoubleSide })); + mesh.position.set(x, y, z - 125); + mesh.rotation.set(rx, ry, rz); + mesh.scale.set(s, s, s); + group.add(mesh); + + // extruded shape + + geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); + + mesh = new THREE.Mesh(geometry, new THREE.MeshPhongMaterial({ color: color })); + mesh.position.set(x, y, z - 75); + mesh.rotation.set(rx, ry, rz); + mesh.scale.set(s, s, s); + group.add(mesh); + + addLineShape(shape, color, x, y, z, rx, ry, rz, s); + } + + function addLineShape(shape, color, x, y, z, rx, ry, rz, s) { + // lines + + shape.autoClose = true; + + const points = shape.getPoints(); + const spacedPoints = shape.getSpacedPoints(50); + + const geometryPoints = new THREE.BufferGeometry().setFromPoints(points); + const geometrySpacedPoints = new THREE.BufferGeometry().setFromPoints(spacedPoints); + + // solid line + + let line = new THREE.Line(geometryPoints, new THREE.LineBasicMaterial({ color: color })); + line.position.set(x, y, z - 25); + line.rotation.set(rx, ry, rz); + line.scale.set(s, s, s); + group.add(line); + + // line from equidistance sampled points + + line = new THREE.Line(geometrySpacedPoints, new THREE.LineBasicMaterial({ color: color })); + line.position.set(x, y, z + 25); + line.rotation.set(rx, ry, rz); + line.scale.set(s, s, s); + group.add(line); + + // vertices from real points + + let particles = new THREE.Points(geometryPoints, new THREE.PointsMaterial({ color: color, size: 4 })); + particles.position.set(x, y, z + 75); + particles.rotation.set(rx, ry, rz); + particles.scale.set(s, s, s); + group.add(particles); + + // equidistance sampled points + + particles = new THREE.Points(geometrySpacedPoints, new THREE.PointsMaterial({ color: color, size: 4 })); + particles.position.set(x, y, z + 125); + particles.rotation.set(rx, ry, rz); + particles.scale.set(s, s, s); + group.add(particles); + } + + // California + + const californiaPts = []; + + californiaPts.push(new THREE.Vector2(610, 320)); + californiaPts.push(new THREE.Vector2(450, 300)); + californiaPts.push(new THREE.Vector2(392, 392)); + californiaPts.push(new THREE.Vector2(266, 438)); + californiaPts.push(new THREE.Vector2(190, 570)); + californiaPts.push(new THREE.Vector2(190, 600)); + californiaPts.push(new THREE.Vector2(160, 620)); + californiaPts.push(new THREE.Vector2(160, 650)); + californiaPts.push(new THREE.Vector2(180, 640)); + californiaPts.push(new THREE.Vector2(165, 680)); + californiaPts.push(new THREE.Vector2(150, 670)); + californiaPts.push(new THREE.Vector2(90, 737)); + californiaPts.push(new THREE.Vector2(80, 795)); + californiaPts.push(new THREE.Vector2(50, 835)); + californiaPts.push(new THREE.Vector2(64, 870)); + californiaPts.push(new THREE.Vector2(60, 945)); + californiaPts.push(new THREE.Vector2(300, 945)); + californiaPts.push(new THREE.Vector2(300, 743)); + californiaPts.push(new THREE.Vector2(600, 473)); + californiaPts.push(new THREE.Vector2(626, 425)); + californiaPts.push(new THREE.Vector2(600, 370)); + californiaPts.push(new THREE.Vector2(610, 320)); + + for (let i = 0; i < californiaPts.length; i++) californiaPts[i].multiplyScalar(0.25); + + const californiaShape = new THREE.Shape(californiaPts); + + // Triangle + + const triangleShape = new THREE.Shape().moveTo(80, 20).lineTo(40, 80).lineTo(120, 80).lineTo(80, 20); // close path + + // Heart + + const x = 0, + y = 0; + + const heartShape = new THREE.Shape() + .moveTo(x + 25, y + 25) + .bezierCurveTo(x + 25, y + 25, x + 20, y, x, y) + .bezierCurveTo(x - 30, y, x - 30, y + 35, x - 30, y + 35) + .bezierCurveTo(x - 30, y + 55, x - 10, y + 77, x + 25, y + 95) + .bezierCurveTo(x + 60, y + 77, x + 80, y + 55, x + 80, y + 35) + .bezierCurveTo(x + 80, y + 35, x + 80, y, x + 50, y) + .bezierCurveTo(x + 35, y, x + 25, y + 25, x + 25, y + 25); + + // Square + + const sqLength = 80; + + const squareShape = new THREE.Shape() + .moveTo(0, 0) + .lineTo(0, sqLength) + .lineTo(sqLength, sqLength) + .lineTo(sqLength, 0) + .lineTo(0, 0); + + // Rounded rectangle + + const roundedRectShape = new THREE.Shape(); + + (function roundedRect(ctx, x, y, width, height, radius) { + ctx.moveTo(x, y + radius); + ctx.lineTo(x, y + height - radius); + ctx.quadraticCurveTo(x, y + height, x + radius, y + height); + ctx.lineTo(x + width - radius, y + height); + ctx.quadraticCurveTo(x + width, y + height, x + width, y + height - radius); + ctx.lineTo(x + width, y + radius); + ctx.quadraticCurveTo(x + width, y, x + width - radius, y); + ctx.lineTo(x + radius, y); + ctx.quadraticCurveTo(x, y, x, y + radius); + })(roundedRectShape, 0, 0, 50, 50, 20); + + // Track + + const trackShape = new THREE.Shape() + .moveTo(40, 40) + .lineTo(40, 160) + .absarc(60, 160, 20, Math.PI, 0, true) + .lineTo(80, 40) + .absarc(60, 40, 20, 2 * Math.PI, Math.PI, true); + + // Circle + + const circleRadius = 40; + const circleShape = new THREE.Shape() + .moveTo(0, circleRadius) + .quadraticCurveTo(circleRadius, circleRadius, circleRadius, 0) + .quadraticCurveTo(circleRadius, -circleRadius, 0, -circleRadius) + .quadraticCurveTo(-circleRadius, -circleRadius, -circleRadius, 0) + .quadraticCurveTo(-circleRadius, circleRadius, 0, circleRadius); + + // Fish + + const fishShape = new THREE.Shape() + .moveTo(x, y) + .quadraticCurveTo(x + 50, y - 80, x + 90, y - 10) + .quadraticCurveTo(x + 100, y - 10, x + 115, y - 40) + .quadraticCurveTo(x + 115, y, x + 115, y + 40) + .quadraticCurveTo(x + 100, y + 10, x + 90, y + 10) + .quadraticCurveTo(x + 50, y + 80, x, y); + + // Arc circle + + const arcShape = new THREE.Shape().moveTo(50, 10).absarc(10, 10, 40, 0, Math.PI * 2, false); + + const holePath = new THREE.Path().moveTo(20, 10).absarc(10, 10, 10, 0, Math.PI * 2, true); + + arcShape.holes.push(holePath); + + // Smiley + + const smileyShape = new THREE.Shape().moveTo(80, 40).absarc(40, 40, 40, 0, Math.PI * 2, false); + + const smileyEye1Path = new THREE.Path().moveTo(35, 20).absellipse(25, 20, 10, 10, 0, Math.PI * 2, true); + + const smileyEye2Path = new THREE.Path().moveTo(65, 20).absarc(55, 20, 10, 0, Math.PI * 2, true); + + const smileyMouthPath = new THREE.Path() + .moveTo(20, 40) + .quadraticCurveTo(40, 60, 60, 40) + .bezierCurveTo(70, 45, 70, 50, 60, 60) + .quadraticCurveTo(40, 80, 20, 60) + .quadraticCurveTo(5, 50, 20, 40); + + smileyShape.holes.push(smileyEye1Path); + smileyShape.holes.push(smileyEye2Path); + smileyShape.holes.push(smileyMouthPath); + + // Spline shape + + const splinepts = []; + splinepts.push(new THREE.Vector2(70, 20)); + splinepts.push(new THREE.Vector2(80, 90)); + splinepts.push(new THREE.Vector2(-30, 70)); + splinepts.push(new THREE.Vector2(0, 0)); + + const splineShape = new THREE.Shape().moveTo(0, 0).splineThru(splinepts); + + const extrudeSettings = { + depth: 8, + bevelEnabled: true, + bevelSegments: 2, + steps: 2, + bevelSize: 1, + bevelThickness: 1, + }; + + // addShape( shape, color, x, y, z, rx, ry,rz, s ); + + addShape(californiaShape, extrudeSettings, 0xf08000, -300, -100, 0, 0, 0, 0, 1); + addShape(triangleShape, extrudeSettings, 0x8080f0, -180, 0, 0, 0, 0, 0, 1); + addShape(roundedRectShape, extrudeSettings, 0x008000, -150, 150, 0, 0, 0, 0, 1); + addShape(trackShape, extrudeSettings, 0x008080, 200, -100, 0, 0, 0, 0, 1); + addShape(squareShape, extrudeSettings, 0x0040f0, 150, 100, 0, 0, 0, 0, 1); + addShape(heartShape, extrudeSettings, 0xf00000, 60, 100, 0, 0, 0, Math.PI, 1); + addShape(circleShape, extrudeSettings, 0x00f000, 120, 250, 0, 0, 0, 0, 1); + addShape(fishShape, extrudeSettings, 0x404040, -60, 200, 0, 0, 0, 0, 1); + addShape(smileyShape, extrudeSettings, 0xf000f0, -200, 250, 0, 0, 0, Math.PI, 1); + addShape(arcShape, extrudeSettings, 0x804000, 150, 0, 0, 0, 0, 0, 1); + addShape(splineShape, extrudeSettings, 0x808080, -50, -100, 0, 0, 0, 0, 1); + + addLineShape(arcShape.holes[0], 0x804000, 150, 0, 0, 0, 0, 0, 1); + + for (let i = 0; i < smileyShape.holes.length; i += 1) { + addLineShape(smileyShape.holes[i], 0xf000f0, -200, 250, 0, 0, 0, Math.PI, 1); + } + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + container.style.touchAction = 'none'; + container.addEventListener('pointerdown', onPointerDown); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function onPointerDown(event) { + if (event.isPrimary === false) return; + + pointerXOnPointerDown = event.clientX - windowHalfX; + targetRotationOnPointerDown = targetRotation; + + document.addEventListener('pointermove', onPointerMove); + document.addEventListener('pointerup', onPointerUp); +} + +function onPointerMove(event) { + if (event.isPrimary === false) return; + + pointerX = event.clientX - windowHalfX; + + targetRotation = targetRotationOnPointerDown + (pointerX - pointerXOnPointerDown) * 0.02; +} + +function onPointerUp() { + if (event.isPrimary === false) return; + + document.removeEventListener('pointermove', onPointerMove); + document.removeEventListener('pointerup', onPointerUp); +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + group.rotation.y += (targetRotation - group.rotation.y) * 0.05; + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_geometry_teapot.ts b/examples-testing/examples/webgl_geometry_teapot.ts new file mode 100644 index 000000000..4c884a559 --- /dev/null +++ b/examples-testing/examples/webgl_geometry_teapot.ts @@ -0,0 +1,180 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { TeapotGeometry } from 'three/addons/geometries/TeapotGeometry.js'; + +let camera, scene, renderer; +let cameraControls; +let effectController; +const teapotSize = 300; +let ambientLight, light; + +let tess = -1; // force initialization +let bBottom; +let bLid; +let bBody; +let bFitLid; +let bNonBlinn; +let shading; + +let teapot, textureCube; +const materials = {}; + +init(); +render(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + const canvasWidth = window.innerWidth; + const canvasHeight = window.innerHeight; + + // CAMERA + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 80000); + camera.position.set(-600, 550, 1300); + + // LIGHTS + ambientLight = new THREE.AmbientLight(0x7c7c7c, 3.0); + + light = new THREE.DirectionalLight(0xffffff, 3.0); + light.position.set(0.32, 0.39, 0.7); + + // RENDERER + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(canvasWidth, canvasHeight); + container.appendChild(renderer.domElement); + + // EVENTS + window.addEventListener('resize', onWindowResize); + + // CONTROLS + cameraControls = new OrbitControls(camera, renderer.domElement); + cameraControls.addEventListener('change', render); + + // TEXTURE MAP + const textureMap = new THREE.TextureLoader().load('textures/uv_grid_opengl.jpg'); + textureMap.wrapS = textureMap.wrapT = THREE.RepeatWrapping; + textureMap.anisotropy = 16; + textureMap.colorSpace = THREE.SRGBColorSpace; + + // REFLECTION MAP + const path = 'textures/cube/pisa/'; + const urls = ['px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png']; + + textureCube = new THREE.CubeTextureLoader().setPath(path).load(urls); + + materials['wireframe'] = new THREE.MeshBasicMaterial({ wireframe: true }); + materials['flat'] = new THREE.MeshPhongMaterial({ specular: 0x000000, flatShading: true, side: THREE.DoubleSide }); + materials['smooth'] = new THREE.MeshLambertMaterial({ side: THREE.DoubleSide }); + materials['glossy'] = new THREE.MeshPhongMaterial({ side: THREE.DoubleSide }); + materials['textured'] = new THREE.MeshPhongMaterial({ map: textureMap, side: THREE.DoubleSide }); + materials['reflective'] = new THREE.MeshPhongMaterial({ envMap: textureCube, side: THREE.DoubleSide }); + + // scene itself + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xaaaaaa); + + scene.add(ambientLight); + scene.add(light); + + // GUI + setupGui(); +} + +// EVENT HANDLERS + +function onWindowResize() { + const canvasWidth = window.innerWidth; + const canvasHeight = window.innerHeight; + + renderer.setSize(canvasWidth, canvasHeight); + + camera.aspect = canvasWidth / canvasHeight; + camera.updateProjectionMatrix(); + + render(); +} + +function setupGui() { + effectController = { + newTess: 15, + bottom: true, + lid: true, + body: true, + fitLid: false, + nonblinn: false, + newShading: 'glossy', + }; + + const gui = new GUI(); + gui.add(effectController, 'newTess', [2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50]) + .name('Tessellation Level') + .onChange(render); + gui.add(effectController, 'lid').name('display lid').onChange(render); + gui.add(effectController, 'body').name('display body').onChange(render); + gui.add(effectController, 'bottom').name('display bottom').onChange(render); + gui.add(effectController, 'fitLid').name('snug lid').onChange(render); + gui.add(effectController, 'nonblinn').name('original scale').onChange(render); + gui.add(effectController, 'newShading', ['wireframe', 'flat', 'smooth', 'glossy', 'textured', 'reflective']) + .name('Shading') + .onChange(render); +} + +// + +function render() { + if ( + effectController.newTess !== tess || + effectController.bottom !== bBottom || + effectController.lid !== bLid || + effectController.body !== bBody || + effectController.fitLid !== bFitLid || + effectController.nonblinn !== bNonBlinn || + effectController.newShading !== shading + ) { + tess = effectController.newTess; + bBottom = effectController.bottom; + bLid = effectController.lid; + bBody = effectController.body; + bFitLid = effectController.fitLid; + bNonBlinn = effectController.nonblinn; + shading = effectController.newShading; + + createNewTeapot(); + } + + // skybox is rendered separately, so that it is always behind the teapot. + if (shading === 'reflective') { + scene.background = textureCube; + } else { + scene.background = null; + } + + renderer.render(scene, camera); +} + +// Whenever the teapot changes, the scene is rebuilt from scratch (not much to it). +function createNewTeapot() { + if (teapot !== undefined) { + teapot.geometry.dispose(); + scene.remove(teapot); + } + + const geometry = new TeapotGeometry( + teapotSize, + tess, + effectController.bottom, + effectController.lid, + effectController.body, + effectController.fitLid, + !effectController.nonblinn, + ); + + teapot = new THREE.Mesh(geometry, materials[shading]); + + scene.add(teapot); +} diff --git a/examples-testing/examples/webgl_geometry_terrain.ts b/examples-testing/examples/webgl_geometry_terrain.ts new file mode 100644 index 000000000..8b6ed84ea --- /dev/null +++ b/examples-testing/examples/webgl_geometry_terrain.ts @@ -0,0 +1,173 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js'; +import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js'; + +let container, stats; +let camera, controls, scene, renderer; +let mesh, texture; + +const worldWidth = 256, + worldDepth = 256; +const clock = new THREE.Clock(); + +init(); + +function init() { + container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 10000); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xefd1b5); + scene.fog = new THREE.FogExp2(0xefd1b5, 0.0025); + + const data = generateHeight(worldWidth, worldDepth); + + camera.position.set(100, 800, -800); + camera.lookAt(-100, 810, -800); + + const geometry = new THREE.PlaneGeometry(7500, 7500, worldWidth - 1, worldDepth - 1); + geometry.rotateX(-Math.PI / 2); + + const vertices = geometry.attributes.position.array; + + for (let i = 0, j = 0, l = vertices.length; i < l; i++, j += 3) { + vertices[j + 1] = data[i] * 10; + } + + texture = new THREE.CanvasTexture(generateTexture(data, worldWidth, worldDepth)); + texture.wrapS = THREE.ClampToEdgeWrapping; + texture.wrapT = THREE.ClampToEdgeWrapping; + texture.colorSpace = THREE.SRGBColorSpace; + + mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ map: texture })); + scene.add(mesh); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + controls = new FirstPersonControls(camera, renderer.domElement); + controls.movementSpeed = 150; + controls.lookSpeed = 0.1; + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + controls.handleResize(); +} + +function generateHeight(width, height) { + let seed = Math.PI / 4; + window.Math.random = function () { + const x = Math.sin(seed++) * 10000; + return x - Math.floor(x); + }; + + const size = width * height, + data = new Uint8Array(size); + const perlin = new ImprovedNoise(), + z = Math.random() * 100; + + let quality = 1; + + for (let j = 0; j < 4; j++) { + for (let i = 0; i < size; i++) { + const x = i % width, + y = ~~(i / width); + data[i] += Math.abs(perlin.noise(x / quality, y / quality, z) * quality * 1.75); + } + + quality *= 5; + } + + return data; +} + +function generateTexture(data, width, height) { + let context, image, imageData, shade; + + const vector3 = new THREE.Vector3(0, 0, 0); + + const sun = new THREE.Vector3(1, 1, 1); + sun.normalize(); + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + + context = canvas.getContext('2d'); + context.fillStyle = '#000'; + context.fillRect(0, 0, width, height); + + image = context.getImageData(0, 0, canvas.width, canvas.height); + imageData = image.data; + + for (let i = 0, j = 0, l = imageData.length; i < l; i += 4, j++) { + vector3.x = data[j - 2] - data[j + 2]; + vector3.y = 2; + vector3.z = data[j - width * 2] - data[j + width * 2]; + vector3.normalize(); + + shade = vector3.dot(sun); + + imageData[i] = (96 + shade * 128) * (0.5 + data[j] * 0.007); + imageData[i + 1] = (32 + shade * 96) * (0.5 + data[j] * 0.007); + imageData[i + 2] = shade * 96 * (0.5 + data[j] * 0.007); + } + + context.putImageData(image, 0, 0); + + // Scaled 4x + + const canvasScaled = document.createElement('canvas'); + canvasScaled.width = width * 4; + canvasScaled.height = height * 4; + + context = canvasScaled.getContext('2d'); + context.scale(4, 4); + context.drawImage(canvas, 0, 0); + + image = context.getImageData(0, 0, canvasScaled.width, canvasScaled.height); + imageData = image.data; + + for (let i = 0, l = imageData.length; i < l; i += 4) { + const v = ~~(Math.random() * 5); + + imageData[i] += v; + imageData[i + 1] += v; + imageData[i + 2] += v; + } + + context.putImageData(image, 0, 0); + + return canvasScaled; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + controls.update(clock.getDelta()); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_geometry_terrain_raycast.ts b/examples-testing/examples/webgl_geometry_terrain_raycast.ts new file mode 100644 index 000000000..f1383c138 --- /dev/null +++ b/examples-testing/examples/webgl_geometry_terrain_raycast.ts @@ -0,0 +1,206 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js'; + +let container, stats; + +let camera, controls, scene, renderer; + +let mesh, texture; + +const worldWidth = 256, + worldDepth = 256, + worldHalfWidth = worldWidth / 2, + worldHalfDepth = worldDepth / 2; + +let helper; + +const raycaster = new THREE.Raycaster(); +const pointer = new THREE.Vector2(); + +init(); + +function init() { + container = document.getElementById('container'); + container.innerHTML = ''; + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xbfd1e5); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 10, 20000); + + controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 1000; + controls.maxDistance = 10000; + controls.maxPolarAngle = Math.PI / 2; + + // + + const data = generateHeight(worldWidth, worldDepth); + + controls.target.y = data[worldHalfWidth + worldHalfDepth * worldWidth] + 500; + camera.position.y = controls.target.y + 2000; + camera.position.x = 2000; + controls.update(); + + const geometry = new THREE.PlaneGeometry(7500, 7500, worldWidth - 1, worldDepth - 1); + geometry.rotateX(-Math.PI / 2); + + const vertices = geometry.attributes.position.array; + + for (let i = 0, j = 0, l = vertices.length; i < l; i++, j += 3) { + vertices[j + 1] = data[i] * 10; + } + + // + + texture = new THREE.CanvasTexture(generateTexture(data, worldWidth, worldDepth)); + texture.wrapS = THREE.ClampToEdgeWrapping; + texture.wrapT = THREE.ClampToEdgeWrapping; + texture.colorSpace = THREE.SRGBColorSpace; + + mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ map: texture })); + scene.add(mesh); + + const geometryHelper = new THREE.ConeGeometry(20, 100, 3); + geometryHelper.translate(0, 50, 0); + geometryHelper.rotateX(Math.PI / 2); + helper = new THREE.Mesh(geometryHelper, new THREE.MeshNormalMaterial()); + scene.add(helper); + + container.addEventListener('pointermove', onPointerMove); + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function generateHeight(width, height) { + const size = width * height, + data = new Uint8Array(size), + perlin = new ImprovedNoise(), + z = Math.random() * 100; + + let quality = 1; + + for (let j = 0; j < 4; j++) { + for (let i = 0; i < size; i++) { + const x = i % width, + y = ~~(i / width); + data[i] += Math.abs(perlin.noise(x / quality, y / quality, z) * quality * 1.75); + } + + quality *= 5; + } + + return data; +} + +function generateTexture(data, width, height) { + // bake lighting into texture + + let context, image, imageData, shade; + + const vector3 = new THREE.Vector3(0, 0, 0); + + const sun = new THREE.Vector3(1, 1, 1); + sun.normalize(); + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + + context = canvas.getContext('2d'); + context.fillStyle = '#000'; + context.fillRect(0, 0, width, height); + + image = context.getImageData(0, 0, canvas.width, canvas.height); + imageData = image.data; + + for (let i = 0, j = 0, l = imageData.length; i < l; i += 4, j++) { + vector3.x = data[j - 2] - data[j + 2]; + vector3.y = 2; + vector3.z = data[j - width * 2] - data[j + width * 2]; + vector3.normalize(); + + shade = vector3.dot(sun); + + imageData[i] = (96 + shade * 128) * (0.5 + data[j] * 0.007); + imageData[i + 1] = (32 + shade * 96) * (0.5 + data[j] * 0.007); + imageData[i + 2] = shade * 96 * (0.5 + data[j] * 0.007); + } + + context.putImageData(image, 0, 0); + + // Scaled 4x + + const canvasScaled = document.createElement('canvas'); + canvasScaled.width = width * 4; + canvasScaled.height = height * 4; + + context = canvasScaled.getContext('2d'); + context.scale(4, 4); + context.drawImage(canvas, 0, 0); + + image = context.getImageData(0, 0, canvasScaled.width, canvasScaled.height); + imageData = image.data; + + for (let i = 0, l = imageData.length; i < l; i += 4) { + const v = ~~(Math.random() * 5); + + imageData[i] += v; + imageData[i + 1] += v; + imageData[i + 2] += v; + } + + context.putImageData(image, 0, 0); + + return canvasScaled; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + renderer.render(scene, camera); +} + +function onPointerMove(event) { + pointer.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1; + pointer.y = -(event.clientY / renderer.domElement.clientHeight) * 2 + 1; + raycaster.setFromCamera(pointer, camera); + + // See if the ray from the camera into the world hits one of our meshes + const intersects = raycaster.intersectObject(mesh); + + // Toggle rotation bool for meshes that we clicked + if (intersects.length > 0) { + helper.position.set(0, 0, 0); + helper.lookAt(intersects[0].face.normal); + + helper.position.copy(intersects[0].point); + } +} diff --git a/examples-testing/examples/webgl_geometry_text.ts b/examples-testing/examples/webgl_geometry_text.ts new file mode 100644 index 000000000..831ebcd6b --- /dev/null +++ b/examples-testing/examples/webgl_geometry_text.ts @@ -0,0 +1,312 @@ +import * as THREE from 'three'; + +import { FontLoader } from 'three/addons/loaders/FontLoader.js'; +import { TextGeometry } from 'three/addons/geometries/TextGeometry.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +THREE.Cache.enabled = true; + +let container; + +let camera, cameraTarget, scene, renderer; + +let group, textMesh1, textMesh2, textGeo, materials; + +let firstLetter = true; + +let text = 'three.js', + bevelEnabled = true, + font = undefined, + fontName = 'optimer', // helvetiker, optimer, gentilis, droid sans, droid serif + fontWeight = 'bold'; // normal bold + +const depth = 20, + size = 70, + hover = 30, + curveSegments = 4, + bevelThickness = 2, + bevelSize = 1.5; + +const mirror = true; + +const fontMap = { + helvetiker: 0, + optimer: 1, + gentilis: 2, + 'droid/droid_sans': 3, + 'droid/droid_serif': 4, +}; + +const weightMap = { + regular: 0, + bold: 1, +}; + +const reverseFontMap = []; +const reverseWeightMap = []; + +for (const i in fontMap) reverseFontMap[fontMap[i]] = i; +for (const i in weightMap) reverseWeightMap[weightMap[i]] = i; + +let targetRotation = 0; +let targetRotationOnPointerDown = 0; + +let pointerX = 0; +let pointerXOnPointerDown = 0; + +let windowHalfX = window.innerWidth / 2; + +let fontIndex = 1; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + // CAMERA + + camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 1, 1500); + camera.position.set(0, 400, 700); + + cameraTarget = new THREE.Vector3(0, 150, 0); + + // SCENE + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x000000); + scene.fog = new THREE.Fog(0x000000, 250, 1400); + + // LIGHTS + + const dirLight = new THREE.DirectionalLight(0xffffff, 0.4); + dirLight.position.set(0, 0, 1).normalize(); + scene.add(dirLight); + + const pointLight = new THREE.PointLight(0xffffff, 4.5, 0, 0); + pointLight.color.setHSL(Math.random(), 1, 0.5); + pointLight.position.set(0, 100, 90); + scene.add(pointLight); + + materials = [ + new THREE.MeshPhongMaterial({ color: 0xffffff, flatShading: true }), // front + new THREE.MeshPhongMaterial({ color: 0xffffff }), // side + ]; + + group = new THREE.Group(); + group.position.y = 100; + + scene.add(group); + + loadFont(); + + const plane = new THREE.Mesh( + new THREE.PlaneGeometry(10000, 10000), + new THREE.MeshBasicMaterial({ color: 0xffffff, opacity: 0.5, transparent: true }), + ); + plane.position.y = 100; + plane.rotation.x = -Math.PI / 2; + scene.add(plane); + + // RENDERER + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // EVENTS + + container.style.touchAction = 'none'; + container.addEventListener('pointerdown', onPointerDown); + + document.addEventListener('keypress', onDocumentKeyPress); + document.addEventListener('keydown', onDocumentKeyDown); + + // + + const params = { + changeColor: function () { + pointLight.color.setHSL(Math.random(), 1, 0.5); + }, + changeFont: function () { + fontIndex++; + + fontName = reverseFontMap[fontIndex % reverseFontMap.length]; + + loadFont(); + }, + changeWeight: function () { + if (fontWeight === 'bold') { + fontWeight = 'regular'; + } else { + fontWeight = 'bold'; + } + + loadFont(); + }, + changeBevel: function () { + bevelEnabled = !bevelEnabled; + + refreshText(); + }, + }; + + // + + const gui = new GUI(); + + gui.add(params, 'changeColor').name('change color'); + gui.add(params, 'changeFont').name('change font'); + gui.add(params, 'changeWeight').name('change weight'); + gui.add(params, 'changeBevel').name('change bevel'); + gui.open(); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function onDocumentKeyDown(event) { + if (firstLetter) { + firstLetter = false; + text = ''; + } + + const keyCode = event.keyCode; + + // backspace + + if (keyCode == 8) { + event.preventDefault(); + + text = text.substring(0, text.length - 1); + refreshText(); + + return false; + } +} + +function onDocumentKeyPress(event) { + const keyCode = event.which; + + // backspace + + if (keyCode == 8) { + event.preventDefault(); + } else { + const ch = String.fromCharCode(keyCode); + text += ch; + + refreshText(); + } +} + +function loadFont() { + const loader = new FontLoader(); + loader.load('fonts/' + fontName + '_' + fontWeight + '.typeface.json', function (response) { + font = response; + + refreshText(); + }); +} + +function createText() { + textGeo = new TextGeometry(text, { + font: font, + + size: size, + depth: depth, + curveSegments: curveSegments, + + bevelThickness: bevelThickness, + bevelSize: bevelSize, + bevelEnabled: bevelEnabled, + }); + + textGeo.computeBoundingBox(); + + const centerOffset = -0.5 * (textGeo.boundingBox.max.x - textGeo.boundingBox.min.x); + + textMesh1 = new THREE.Mesh(textGeo, materials); + + textMesh1.position.x = centerOffset; + textMesh1.position.y = hover; + textMesh1.position.z = 0; + + textMesh1.rotation.x = 0; + textMesh1.rotation.y = Math.PI * 2; + + group.add(textMesh1); + + if (mirror) { + textMesh2 = new THREE.Mesh(textGeo, materials); + + textMesh2.position.x = centerOffset; + textMesh2.position.y = -hover; + textMesh2.position.z = depth; + + textMesh2.rotation.x = Math.PI; + textMesh2.rotation.y = Math.PI * 2; + + group.add(textMesh2); + } +} + +function refreshText() { + group.remove(textMesh1); + if (mirror) group.remove(textMesh2); + + if (!text) return; + + createText(); +} + +function onPointerDown(event) { + if (event.isPrimary === false) return; + + pointerXOnPointerDown = event.clientX - windowHalfX; + targetRotationOnPointerDown = targetRotation; + + document.addEventListener('pointermove', onPointerMove); + document.addEventListener('pointerup', onPointerUp); +} + +function onPointerMove(event) { + if (event.isPrimary === false) return; + + pointerX = event.clientX - windowHalfX; + + targetRotation = targetRotationOnPointerDown + (pointerX - pointerXOnPointerDown) * 0.02; +} + +function onPointerUp() { + if (event.isPrimary === false) return; + + document.removeEventListener('pointermove', onPointerMove); + document.removeEventListener('pointerup', onPointerUp); +} + +// + +function animate() { + group.rotation.y += (targetRotation - group.rotation.y) * 0.05; + + camera.lookAt(cameraTarget); + + renderer.clear(); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_geometry_text_shapes.ts b/examples-testing/examples/webgl_geometry_text_shapes.ts new file mode 100644 index 000000000..adfb6008d --- /dev/null +++ b/examples-testing/examples/webgl_geometry_text_shapes.ts @@ -0,0 +1,112 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { FontLoader } from 'three/addons/loaders/FontLoader.js'; + +let camera, scene, renderer; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.set(0, -400, 600); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + const loader = new FontLoader(); + loader.load('fonts/helvetiker_regular.typeface.json', function (font) { + const color = 0x006699; + + const matDark = new THREE.LineBasicMaterial({ + color: color, + side: THREE.DoubleSide, + }); + + const matLite = new THREE.MeshBasicMaterial({ + color: color, + transparent: true, + opacity: 0.4, + side: THREE.DoubleSide, + }); + + const message = ' Three.js\nSimple text.'; + + const shapes = font.generateShapes(message, 100); + + const geometry = new THREE.ShapeGeometry(shapes); + + geometry.computeBoundingBox(); + + const xMid = -0.5 * (geometry.boundingBox.max.x - geometry.boundingBox.min.x); + + geometry.translate(xMid, 0, 0); + + // make shape ( N.B. edge view not visible ) + + const text = new THREE.Mesh(geometry, matLite); + text.position.z = -150; + scene.add(text); + + // make line shape ( N.B. edge view remains visible ) + + const holeShapes = []; + + for (let i = 0; i < shapes.length; i++) { + const shape = shapes[i]; + + if (shape.holes && shape.holes.length > 0) { + for (let j = 0; j < shape.holes.length; j++) { + const hole = shape.holes[j]; + holeShapes.push(hole); + } + } + } + + shapes.push.apply(shapes, holeShapes); + + const lineText = new THREE.Object3D(); + + for (let i = 0; i < shapes.length; i++) { + const shape = shapes[i]; + + const points = shape.getPoints(); + const geometry = new THREE.BufferGeometry().setFromPoints(points); + + geometry.translate(xMid, 0, 0); + + const lineMesh = new THREE.Line(geometry, matDark); + lineText.add(lineMesh); + } + + scene.add(lineText); + + render(); + }); //end load function + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 0, 0); + controls.update(); + + controls.addEventListener('change', render); + + window.addEventListener('resize', onWindowResize); +} // end init + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_geometry_text_stroke.ts b/examples-testing/examples/webgl_geometry_text_stroke.ts new file mode 100644 index 000000000..9a1983253 --- /dev/null +++ b/examples-testing/examples/webgl_geometry_text_stroke.ts @@ -0,0 +1,116 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { SVGLoader } from 'three/addons/loaders/SVGLoader.js'; +import { FontLoader } from 'three/addons/loaders/FontLoader.js'; + +let camera, scene, renderer; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.set(0, -400, 600); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + const loader = new FontLoader(); + loader.load('fonts/helvetiker_regular.typeface.json', function (font) { + const color = new THREE.Color(0x006699); + + const matDark = new THREE.MeshBasicMaterial({ + color: color, + side: THREE.DoubleSide, + }); + + const matLite = new THREE.MeshBasicMaterial({ + color: color, + transparent: true, + opacity: 0.4, + side: THREE.DoubleSide, + }); + + const message = ' Three.js\nStroke text.'; + + const shapes = font.generateShapes(message, 100); + + const geometry = new THREE.ShapeGeometry(shapes); + + geometry.computeBoundingBox(); + + const xMid = -0.5 * (geometry.boundingBox.max.x - geometry.boundingBox.min.x); + + geometry.translate(xMid, 0, 0); + + // make shape ( N.B. edge view not visible ) + + const text = new THREE.Mesh(geometry, matLite); + text.position.z = -150; + scene.add(text); + + // make line shape ( N.B. edge view remains visible ) + + const holeShapes = []; + + for (let i = 0; i < shapes.length; i++) { + const shape = shapes[i]; + + if (shape.holes && shape.holes.length > 0) { + for (let j = 0; j < shape.holes.length; j++) { + const hole = shape.holes[j]; + holeShapes.push(hole); + } + } + } + + shapes.push.apply(shapes, holeShapes); + + const style = SVGLoader.getStrokeStyle(5, color.getStyle()); + + const strokeText = new THREE.Group(); + + for (let i = 0; i < shapes.length; i++) { + const shape = shapes[i]; + + const points = shape.getPoints(); + + const geometry = SVGLoader.pointsToStroke(points, style); + + geometry.translate(xMid, 0, 0); + + const strokeMesh = new THREE.Mesh(geometry, matDark); + strokeText.add(strokeMesh); + } + + scene.add(strokeText); + + render(); + }); //end load function + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 0, 0); + controls.update(); + + controls.addEventListener('change', render); + + window.addEventListener('resize', onWindowResize); +} // end init + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_gpgpu_birds.ts b/examples-testing/examples/webgl_gpgpu_birds.ts new file mode 100644 index 000000000..20a5e0d97 --- /dev/null +++ b/examples-testing/examples/webgl_gpgpu_birds.ts @@ -0,0 +1,313 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { GPUComputationRenderer } from 'three/addons/misc/GPUComputationRenderer.js'; + +/* TEXTURE WIDTH FOR SIMULATION */ +const WIDTH = 32; + +const BIRDS = WIDTH * WIDTH; + +// Custom Geometry - using 3 triangles each. No UVs, no normals currently. +class BirdGeometry extends THREE.BufferGeometry { + constructor() { + super(); + + const trianglesPerBird = 3; + const triangles = BIRDS * trianglesPerBird; + const points = triangles * 3; + + const vertices = new THREE.BufferAttribute(new Float32Array(points * 3), 3); + const birdColors = new THREE.BufferAttribute(new Float32Array(points * 3), 3); + const references = new THREE.BufferAttribute(new Float32Array(points * 2), 2); + const birdVertex = new THREE.BufferAttribute(new Float32Array(points), 1); + + this.setAttribute('position', vertices); + this.setAttribute('birdColor', birdColors); + this.setAttribute('reference', references); + this.setAttribute('birdVertex', birdVertex); + + // this.setAttribute( 'normal', new Float32Array( points * 3 ), 3 ); + + let v = 0; + + function verts_push() { + for (let i = 0; i < arguments.length; i++) { + vertices.array[v++] = arguments[i]; + } + } + + const wingsSpan = 20; + + for (let f = 0; f < BIRDS; f++) { + // Body + + verts_push(0, -0, -20, 0, 4, -20, 0, 0, 30); + + // Wings + + verts_push(0, 0, -15, -wingsSpan, 0, 0, 0, 0, 15); + + verts_push(0, 0, 15, wingsSpan, 0, 0, 0, 0, -15); + } + + for (let v = 0; v < triangles * 3; v++) { + const triangleIndex = ~~(v / 3); + const birdIndex = ~~(triangleIndex / trianglesPerBird); + const x = (birdIndex % WIDTH) / WIDTH; + const y = ~~(birdIndex / WIDTH) / WIDTH; + + const c = new THREE.Color(0x666666 + (~~(v / 9) / BIRDS) * 0x666666); + + birdColors.array[v * 3 + 0] = c.r; + birdColors.array[v * 3 + 1] = c.g; + birdColors.array[v * 3 + 2] = c.b; + + references.array[v * 2] = x; + references.array[v * 2 + 1] = y; + + birdVertex.array[v] = v % 9; + } + + this.scale(0.2, 0.2, 0.2); + } +} + +// + +let container, stats; +let camera, scene, renderer; +let mouseX = 0, + mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +const BOUNDS = 800, + BOUNDS_HALF = BOUNDS / 2; + +let last = performance.now(); + +let gpuCompute; +let velocityVariable; +let positionVariable; +let positionUniforms; +let velocityUniforms; +let birdUniforms; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 3000); + camera.position.z = 350; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + scene.fog = new THREE.Fog(0xffffff, 100, 1000); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + initComputeRenderer(); + + stats = new Stats(); + container.appendChild(stats.dom); + + container.style.touchAction = 'none'; + container.addEventListener('pointermove', onPointerMove); + + // + + window.addEventListener('resize', onWindowResize); + + const gui = new GUI(); + + const effectController = { + separation: 20.0, + alignment: 20.0, + cohesion: 20.0, + freedom: 0.75, + }; + + const valuesChanger = function () { + velocityUniforms['separationDistance'].value = effectController.separation; + velocityUniforms['alignmentDistance'].value = effectController.alignment; + velocityUniforms['cohesionDistance'].value = effectController.cohesion; + velocityUniforms['freedomFactor'].value = effectController.freedom; + }; + + valuesChanger(); + + gui.add(effectController, 'separation', 0.0, 100.0, 1.0).onChange(valuesChanger); + gui.add(effectController, 'alignment', 0.0, 100, 0.001).onChange(valuesChanger); + gui.add(effectController, 'cohesion', 0.0, 100, 0.025).onChange(valuesChanger); + gui.close(); + + initBirds(); +} + +function initComputeRenderer() { + gpuCompute = new GPUComputationRenderer(WIDTH, WIDTH, renderer); + + const dtPosition = gpuCompute.createTexture(); + const dtVelocity = gpuCompute.createTexture(); + fillPositionTexture(dtPosition); + fillVelocityTexture(dtVelocity); + + velocityVariable = gpuCompute.addVariable( + 'textureVelocity', + document.getElementById('fragmentShaderVelocity').textContent, + dtVelocity, + ); + positionVariable = gpuCompute.addVariable( + 'texturePosition', + document.getElementById('fragmentShaderPosition').textContent, + dtPosition, + ); + + gpuCompute.setVariableDependencies(velocityVariable, [positionVariable, velocityVariable]); + gpuCompute.setVariableDependencies(positionVariable, [positionVariable, velocityVariable]); + + positionUniforms = positionVariable.material.uniforms; + velocityUniforms = velocityVariable.material.uniforms; + + positionUniforms['time'] = { value: 0.0 }; + positionUniforms['delta'] = { value: 0.0 }; + velocityUniforms['time'] = { value: 1.0 }; + velocityUniforms['delta'] = { value: 0.0 }; + velocityUniforms['testing'] = { value: 1.0 }; + velocityUniforms['separationDistance'] = { value: 1.0 }; + velocityUniforms['alignmentDistance'] = { value: 1.0 }; + velocityUniforms['cohesionDistance'] = { value: 1.0 }; + velocityUniforms['freedomFactor'] = { value: 1.0 }; + velocityUniforms['predator'] = { value: new THREE.Vector3() }; + velocityVariable.material.defines.BOUNDS = BOUNDS.toFixed(2); + + velocityVariable.wrapS = THREE.RepeatWrapping; + velocityVariable.wrapT = THREE.RepeatWrapping; + positionVariable.wrapS = THREE.RepeatWrapping; + positionVariable.wrapT = THREE.RepeatWrapping; + + const error = gpuCompute.init(); + + if (error !== null) { + console.error(error); + } +} + +function initBirds() { + const geometry = new BirdGeometry(); + + // For Vertex and Fragment + birdUniforms = { + color: { value: new THREE.Color(0xff2200) }, + texturePosition: { value: null }, + textureVelocity: { value: null }, + time: { value: 1.0 }, + delta: { value: 0.0 }, + }; + + // THREE.ShaderMaterial + const material = new THREE.ShaderMaterial({ + uniforms: birdUniforms, + vertexShader: document.getElementById('birdVS').textContent, + fragmentShader: document.getElementById('birdFS').textContent, + side: THREE.DoubleSide, + }); + + const birdMesh = new THREE.Mesh(geometry, material); + birdMesh.rotation.y = Math.PI / 2; + birdMesh.matrixAutoUpdate = false; + birdMesh.updateMatrix(); + + scene.add(birdMesh); +} + +function fillPositionTexture(texture) { + const theArray = texture.image.data; + + for (let k = 0, kl = theArray.length; k < kl; k += 4) { + const x = Math.random() * BOUNDS - BOUNDS_HALF; + const y = Math.random() * BOUNDS - BOUNDS_HALF; + const z = Math.random() * BOUNDS - BOUNDS_HALF; + + theArray[k + 0] = x; + theArray[k + 1] = y; + theArray[k + 2] = z; + theArray[k + 3] = 1; + } +} + +function fillVelocityTexture(texture) { + const theArray = texture.image.data; + + for (let k = 0, kl = theArray.length; k < kl; k += 4) { + const x = Math.random() - 0.5; + const y = Math.random() - 0.5; + const z = Math.random() - 0.5; + + theArray[k + 0] = x * 10; + theArray[k + 1] = y * 10; + theArray[k + 2] = z * 10; + theArray[k + 3] = 1; + } +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onPointerMove(event) { + if (event.isPrimary === false) return; + + mouseX = event.clientX - windowHalfX; + mouseY = event.clientY - windowHalfY; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + const now = performance.now(); + let delta = (now - last) / 1000; + + if (delta > 1) delta = 1; // safety cap on large deltas + last = now; + + positionUniforms['time'].value = now; + positionUniforms['delta'].value = delta; + velocityUniforms['time'].value = now; + velocityUniforms['delta'].value = delta; + birdUniforms['time'].value = now; + birdUniforms['delta'].value = delta; + + velocityUniforms['predator'].value.set((0.5 * mouseX) / windowHalfX, (-0.5 * mouseY) / windowHalfY, 0); + + mouseX = 10000; + mouseY = 10000; + + gpuCompute.compute(); + + birdUniforms['texturePosition'].value = gpuCompute.getCurrentRenderTarget(positionVariable).texture; + birdUniforms['textureVelocity'].value = gpuCompute.getCurrentRenderTarget(velocityVariable).texture; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_gpgpu_birds_gltf.ts b/examples-testing/examples/webgl_gpgpu_birds_gltf.ts new file mode 100644 index 000000000..3176b95a9 --- /dev/null +++ b/examples-testing/examples/webgl_gpgpu_birds_gltf.ts @@ -0,0 +1,415 @@ +import * as THREE from 'three'; +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { GPUComputationRenderer } from 'three/addons/misc/GPUComputationRenderer.js'; + +/* TEXTURE WIDTH FOR SIMULATION */ +const WIDTH = 64; +const BIRDS = WIDTH * WIDTH; + +/* BAKE ANIMATION INTO TEXTURE and CREATE GEOMETRY FROM BASE MODEL */ +const BirdGeometry = new THREE.BufferGeometry(); +let textureAnimation, durationAnimation, birdMesh, materialShader, indicesPerBird; + +function nextPowerOf2(n) { + return Math.pow(2, Math.ceil(Math.log(n) / Math.log(2))); +} + +Math.lerp = function (value1, value2, amount) { + amount = Math.max(Math.min(amount, 1), 0); + return value1 + (value2 - value1) * amount; +}; + +const gltfs = ['models/gltf/Parrot.glb', 'models/gltf/Flamingo.glb']; +const colors = [0xccffff, 0xffdeff]; +const sizes = [0.2, 0.1]; +const selectModel = Math.floor(Math.random() * gltfs.length); +new GLTFLoader().load(gltfs[selectModel], function (gltf) { + const animations = gltf.animations; + durationAnimation = Math.round(animations[0].duration * 60); + const birdGeo = gltf.scene.children[0].geometry; + const morphAttributes = birdGeo.morphAttributes.position; + const tHeight = nextPowerOf2(durationAnimation); + const tWidth = nextPowerOf2(birdGeo.getAttribute('position').count); + indicesPerBird = birdGeo.index.count; + const tData = new Float32Array(4 * tWidth * tHeight); + + for (let i = 0; i < tWidth; i++) { + for (let j = 0; j < tHeight; j++) { + const offset = j * tWidth * 4; + + const curMorph = Math.floor((j / durationAnimation) * morphAttributes.length); + const nextMorph = + (Math.floor((j / durationAnimation) * morphAttributes.length) + 1) % morphAttributes.length; + const lerpAmount = ((j / durationAnimation) * morphAttributes.length) % 1; + + if (j < durationAnimation) { + let d0, d1; + + d0 = morphAttributes[curMorph].array[i * 3]; + d1 = morphAttributes[nextMorph].array[i * 3]; + + if (d0 !== undefined && d1 !== undefined) tData[offset + i * 4] = Math.lerp(d0, d1, lerpAmount); + + d0 = morphAttributes[curMorph].array[i * 3 + 1]; + d1 = morphAttributes[nextMorph].array[i * 3 + 1]; + + if (d0 !== undefined && d1 !== undefined) tData[offset + i * 4 + 1] = Math.lerp(d0, d1, lerpAmount); + + d0 = morphAttributes[curMorph].array[i * 3 + 2]; + d1 = morphAttributes[nextMorph].array[i * 3 + 2]; + + if (d0 !== undefined && d1 !== undefined) tData[offset + i * 4 + 2] = Math.lerp(d0, d1, lerpAmount); + + tData[offset + i * 4 + 3] = 1; + } + } + } + + textureAnimation = new THREE.DataTexture(tData, tWidth, tHeight, THREE.RGBAFormat, THREE.FloatType); + textureAnimation.needsUpdate = true; + + const vertices = [], + color = [], + reference = [], + seeds = [], + indices = []; + const totalVertices = birdGeo.getAttribute('position').count * 3 * BIRDS; + for (let i = 0; i < totalVertices; i++) { + const bIndex = i % (birdGeo.getAttribute('position').count * 3); + vertices.push(birdGeo.getAttribute('position').array[bIndex]); + color.push(birdGeo.getAttribute('color').array[bIndex]); + } + + let r = Math.random(); + for (let i = 0; i < birdGeo.getAttribute('position').count * BIRDS; i++) { + const bIndex = i % birdGeo.getAttribute('position').count; + const bird = Math.floor(i / birdGeo.getAttribute('position').count); + if (bIndex == 0) r = Math.random(); + const j = ~~bird; + const x = (j % WIDTH) / WIDTH; + const y = ~~(j / WIDTH) / WIDTH; + reference.push(x, y, bIndex / tWidth, durationAnimation / tHeight); + seeds.push(bird, r, Math.random(), Math.random()); + } + + for (let i = 0; i < birdGeo.index.array.length * BIRDS; i++) { + const offset = Math.floor(i / birdGeo.index.array.length) * birdGeo.getAttribute('position').count; + indices.push(birdGeo.index.array[i % birdGeo.index.array.length] + offset); + } + + BirdGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3)); + BirdGeometry.setAttribute('birdColor', new THREE.BufferAttribute(new Float32Array(color), 3)); + BirdGeometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(color), 3)); + BirdGeometry.setAttribute('reference', new THREE.BufferAttribute(new Float32Array(reference), 4)); + BirdGeometry.setAttribute('seeds', new THREE.BufferAttribute(new Float32Array(seeds), 4)); + + BirdGeometry.setIndex(indices); + + init(); +}); + +let container, stats; +let camera, scene, renderer; +let mouseX = 0, + mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +const BOUNDS = 800, + BOUNDS_HALF = BOUNDS / 2; + +let last = performance.now(); + +let gpuCompute; +let velocityVariable; +let positionVariable; +let positionUniforms; +let velocityUniforms; + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 3000); + camera.position.z = 350; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(colors[selectModel]); + scene.fog = new THREE.Fog(colors[selectModel], 100, 1000); + + // LIGHTS + + const hemiLight = new THREE.HemisphereLight(colors[selectModel], 0xffffff, 4.5); + hemiLight.color.setHSL(0.6, 1, 0.6, THREE.SRGBColorSpace); + hemiLight.groundColor.setHSL(0.095, 1, 0.75, THREE.SRGBColorSpace); + hemiLight.position.set(0, 50, 0); + scene.add(hemiLight); + + const dirLight = new THREE.DirectionalLight(0x00ced1, 2.0); + dirLight.color.setHSL(0.1, 1, 0.95, THREE.SRGBColorSpace); + dirLight.position.set(-1, 1.75, 1); + dirLight.position.multiplyScalar(30); + scene.add(dirLight); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + initComputeRenderer(); + + stats = new Stats(); + container.appendChild(stats.dom); + + container.style.touchAction = 'none'; + container.addEventListener('pointermove', onPointerMove); + + window.addEventListener('resize', onWindowResize); + + const gui = new GUI(); + + const effectController = { + separation: 20.0, + alignment: 20.0, + cohesion: 20.0, + freedom: 0.75, + size: sizes[selectModel], + count: Math.floor(BIRDS / 4), + }; + + const valuesChanger = function () { + velocityUniforms['separationDistance'].value = effectController.separation; + velocityUniforms['alignmentDistance'].value = effectController.alignment; + velocityUniforms['cohesionDistance'].value = effectController.cohesion; + velocityUniforms['freedomFactor'].value = effectController.freedom; + if (materialShader) materialShader.uniforms['size'].value = effectController.size; + BirdGeometry.setDrawRange(0, indicesPerBird * effectController.count); + }; + + valuesChanger(); + + gui.add(effectController, 'separation', 0.0, 100.0, 1.0).onChange(valuesChanger); + gui.add(effectController, 'alignment', 0.0, 100, 0.001).onChange(valuesChanger); + gui.add(effectController, 'cohesion', 0.0, 100, 0.025).onChange(valuesChanger); + gui.add(effectController, 'size', 0, 1, 0.01).onChange(valuesChanger); + gui.add(effectController, 'count', 0, BIRDS, 1).onChange(valuesChanger); + gui.close(); + + initBirds(effectController); +} + +function initComputeRenderer() { + gpuCompute = new GPUComputationRenderer(WIDTH, WIDTH, renderer); + + const dtPosition = gpuCompute.createTexture(); + const dtVelocity = gpuCompute.createTexture(); + fillPositionTexture(dtPosition); + fillVelocityTexture(dtVelocity); + + velocityVariable = gpuCompute.addVariable( + 'textureVelocity', + document.getElementById('fragmentShaderVelocity').textContent, + dtVelocity, + ); + positionVariable = gpuCompute.addVariable( + 'texturePosition', + document.getElementById('fragmentShaderPosition').textContent, + dtPosition, + ); + + gpuCompute.setVariableDependencies(velocityVariable, [positionVariable, velocityVariable]); + gpuCompute.setVariableDependencies(positionVariable, [positionVariable, velocityVariable]); + + positionUniforms = positionVariable.material.uniforms; + velocityUniforms = velocityVariable.material.uniforms; + + positionUniforms['time'] = { value: 0.0 }; + positionUniforms['delta'] = { value: 0.0 }; + velocityUniforms['time'] = { value: 1.0 }; + velocityUniforms['delta'] = { value: 0.0 }; + velocityUniforms['testing'] = { value: 1.0 }; + velocityUniforms['separationDistance'] = { value: 1.0 }; + velocityUniforms['alignmentDistance'] = { value: 1.0 }; + velocityUniforms['cohesionDistance'] = { value: 1.0 }; + velocityUniforms['freedomFactor'] = { value: 1.0 }; + velocityUniforms['predator'] = { value: new THREE.Vector3() }; + velocityVariable.material.defines.BOUNDS = BOUNDS.toFixed(2); + + velocityVariable.wrapS = THREE.RepeatWrapping; + velocityVariable.wrapT = THREE.RepeatWrapping; + positionVariable.wrapS = THREE.RepeatWrapping; + positionVariable.wrapT = THREE.RepeatWrapping; + + const error = gpuCompute.init(); + + if (error !== null) { + console.error(error); + } +} + +function initBirds(effectController) { + const geometry = BirdGeometry; + + const m = new THREE.MeshStandardMaterial({ + vertexColors: true, + flatShading: true, + roughness: 1, + metalness: 0, + }); + + m.onBeforeCompile = shader => { + shader.uniforms.texturePosition = { value: null }; + shader.uniforms.textureVelocity = { value: null }; + shader.uniforms.textureAnimation = { value: textureAnimation }; + shader.uniforms.time = { value: 1.0 }; + shader.uniforms.size = { value: effectController.size }; + shader.uniforms.delta = { value: 0.0 }; + + let token = '#define STANDARD'; + + let insert = /* glsl */ ` + attribute vec4 reference; + attribute vec4 seeds; + attribute vec3 birdColor; + uniform sampler2D texturePosition; + uniform sampler2D textureVelocity; + uniform sampler2D textureAnimation; + uniform float size; + uniform float time; + `; + + shader.vertexShader = shader.vertexShader.replace(token, token + insert); + + token = '#include '; + + insert = /* glsl */ ` + vec4 tmpPos = texture2D( texturePosition, reference.xy ); + + vec3 pos = tmpPos.xyz; + vec3 velocity = normalize(texture2D( textureVelocity, reference.xy ).xyz); + vec3 aniPos = texture2D( textureAnimation, vec2( reference.z, mod( time + ( seeds.x ) * ( ( 0.0004 + seeds.y / 10000.0) + normalize( velocity ) / 20000.0 ), reference.w ) ) ).xyz; + vec3 newPosition = position; + + newPosition = mat3( modelMatrix ) * ( newPosition + aniPos ); + newPosition *= size + seeds.y * size * 0.2; + + velocity.z *= -1.; + float xz = length( velocity.xz ); + float xyz = 1.; + float x = sqrt( 1. - velocity.y * velocity.y ); + + float cosry = velocity.x / xz; + float sinry = velocity.z / xz; + + float cosrz = x / xyz; + float sinrz = velocity.y / xyz; + + mat3 maty = mat3( cosry, 0, -sinry, 0 , 1, 0 , sinry, 0, cosry ); + mat3 matz = mat3( cosrz , sinrz, 0, -sinrz, cosrz, 0, 0 , 0 , 1 ); + + newPosition = maty * matz * newPosition; + newPosition += pos; + + vec3 transformed = vec3( newPosition ); + `; + + shader.vertexShader = shader.vertexShader.replace(token, insert); + + materialShader = shader; + }; + + birdMesh = new THREE.Mesh(geometry, m); + birdMesh.rotation.y = Math.PI / 2; + + birdMesh.castShadow = true; + birdMesh.receiveShadow = true; + + scene.add(birdMesh); +} + +function fillPositionTexture(texture) { + const theArray = texture.image.data; + + for (let k = 0, kl = theArray.length; k < kl; k += 4) { + const x = Math.random() * BOUNDS - BOUNDS_HALF; + const y = Math.random() * BOUNDS - BOUNDS_HALF; + const z = Math.random() * BOUNDS - BOUNDS_HALF; + + theArray[k + 0] = x; + theArray[k + 1] = y; + theArray[k + 2] = z; + theArray[k + 3] = 1; + } +} + +function fillVelocityTexture(texture) { + const theArray = texture.image.data; + + for (let k = 0, kl = theArray.length; k < kl; k += 4) { + const x = Math.random() - 0.5; + const y = Math.random() - 0.5; + const z = Math.random() - 0.5; + + theArray[k + 0] = x * 10; + theArray[k + 1] = y * 10; + theArray[k + 2] = z * 10; + theArray[k + 3] = 1; + } +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onPointerMove(event) { + if (event.isPrimary === false) return; + + mouseX = event.clientX - windowHalfX; + mouseY = event.clientY - windowHalfY; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + const now = performance.now(); + let delta = (now - last) / 1000; + + if (delta > 1) delta = 1; // safety cap on large deltas + last = now; + + positionUniforms['time'].value = now; + positionUniforms['delta'].value = delta; + velocityUniforms['time'].value = now; + velocityUniforms['delta'].value = delta; + if (materialShader) materialShader.uniforms['time'].value = now / 1000; + if (materialShader) materialShader.uniforms['delta'].value = delta; + + velocityUniforms['predator'].value.set((0.5 * mouseX) / windowHalfX, (-0.5 * mouseY) / windowHalfY, 0); + + mouseX = 10000; + mouseY = 10000; + + gpuCompute.compute(); + + if (materialShader) + materialShader.uniforms['texturePosition'].value = gpuCompute.getCurrentRenderTarget(positionVariable).texture; + if (materialShader) + materialShader.uniforms['textureVelocity'].value = gpuCompute.getCurrentRenderTarget(velocityVariable).texture; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_gpgpu_protoplanet.ts b/examples-testing/examples/webgl_gpgpu_protoplanet.ts new file mode 100644 index 000000000..30444ddba --- /dev/null +++ b/examples-testing/examples/webgl_gpgpu_protoplanet.ts @@ -0,0 +1,280 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GPUComputationRenderer } from 'three/addons/misc/GPUComputationRenderer.js'; + +// Texture width for simulation (each texel is a debris particle) +const WIDTH = 64; + +let container, stats; +let camera, scene, renderer, geometry; + +const PARTICLES = WIDTH * WIDTH; + +let gpuCompute; +let velocityVariable; +let positionVariable; +let velocityUniforms; +let particleUniforms; +let effectController; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 5, 15000); + camera.position.y = 120; + camera.position.z = 400; + + scene = new THREE.Scene(); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 100; + controls.maxDistance = 1000; + + effectController = { + // Can be changed dynamically + gravityConstant: 100.0, + density: 0.45, + + // Must restart simulation + radius: 300, + height: 8, + exponent: 0.4, + maxMass: 15.0, + velocity: 70, + velocityExponent: 0.2, + randVelocity: 0.001, + }; + + initComputeRenderer(); + + stats = new Stats(); + container.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); + + initGUI(); + + initProtoplanets(); + + dynamicValuesChanger(); +} + +function initComputeRenderer() { + gpuCompute = new GPUComputationRenderer(WIDTH, WIDTH, renderer); + + const dtPosition = gpuCompute.createTexture(); + const dtVelocity = gpuCompute.createTexture(); + + fillTextures(dtPosition, dtVelocity); + + velocityVariable = gpuCompute.addVariable( + 'textureVelocity', + document.getElementById('computeShaderVelocity').textContent, + dtVelocity, + ); + positionVariable = gpuCompute.addVariable( + 'texturePosition', + document.getElementById('computeShaderPosition').textContent, + dtPosition, + ); + + gpuCompute.setVariableDependencies(velocityVariable, [positionVariable, velocityVariable]); + gpuCompute.setVariableDependencies(positionVariable, [positionVariable, velocityVariable]); + + velocityUniforms = velocityVariable.material.uniforms; + + velocityUniforms['gravityConstant'] = { value: 0.0 }; + velocityUniforms['density'] = { value: 0.0 }; + + const error = gpuCompute.init(); + + if (error !== null) { + console.error(error); + } +} + +function restartSimulation() { + const dtPosition = gpuCompute.createTexture(); + const dtVelocity = gpuCompute.createTexture(); + + fillTextures(dtPosition, dtVelocity); + + gpuCompute.renderTexture(dtPosition, positionVariable.renderTargets[0]); + gpuCompute.renderTexture(dtPosition, positionVariable.renderTargets[1]); + gpuCompute.renderTexture(dtVelocity, velocityVariable.renderTargets[0]); + gpuCompute.renderTexture(dtVelocity, velocityVariable.renderTargets[1]); +} + +function initProtoplanets() { + geometry = new THREE.BufferGeometry(); + + const positions = new Float32Array(PARTICLES * 3); + let p = 0; + + for (let i = 0; i < PARTICLES; i++) { + positions[p++] = (Math.random() * 2 - 1) * effectController.radius; + positions[p++] = 0; //( Math.random() * 2 - 1 ) * effectController.radius; + positions[p++] = (Math.random() * 2 - 1) * effectController.radius; + } + + const uvs = new Float32Array(PARTICLES * 2); + p = 0; + + for (let j = 0; j < WIDTH; j++) { + for (let i = 0; i < WIDTH; i++) { + uvs[p++] = i / (WIDTH - 1); + uvs[p++] = j / (WIDTH - 1); + } + } + + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2)); + + particleUniforms = { + texturePosition: { value: null }, + textureVelocity: { value: null }, + cameraConstant: { value: getCameraConstant(camera) }, + density: { value: 0.0 }, + }; + + // THREE.ShaderMaterial + const material = new THREE.ShaderMaterial({ + uniforms: particleUniforms, + vertexShader: document.getElementById('particleVertexShader').textContent, + fragmentShader: document.getElementById('particleFragmentShader').textContent, + }); + + const particles = new THREE.Points(geometry, material); + particles.matrixAutoUpdate = false; + particles.updateMatrix(); + + scene.add(particles); +} + +function fillTextures(texturePosition, textureVelocity) { + const posArray = texturePosition.image.data; + const velArray = textureVelocity.image.data; + + const radius = effectController.radius; + const height = effectController.height; + const exponent = effectController.exponent; + const maxMass = (effectController.maxMass * 1024) / PARTICLES; + const maxVel = effectController.velocity; + const velExponent = effectController.velocityExponent; + const randVel = effectController.randVelocity; + + for (let k = 0, kl = posArray.length; k < kl; k += 4) { + // Position + let x, z, rr; + + do { + x = Math.random() * 2 - 1; + z = Math.random() * 2 - 1; + rr = x * x + z * z; + } while (rr > 1); + + rr = Math.sqrt(rr); + + const rExp = radius * Math.pow(rr, exponent); + + // Velocity + const vel = maxVel * Math.pow(rr, velExponent); + + const vx = vel * z + (Math.random() * 2 - 1) * randVel; + const vy = (Math.random() * 2 - 1) * randVel * 0.05; + const vz = -vel * x + (Math.random() * 2 - 1) * randVel; + + x *= rExp; + z *= rExp; + const y = (Math.random() * 2 - 1) * height; + + const mass = Math.random() * maxMass + 1; + + // Fill in texture values + posArray[k + 0] = x; + posArray[k + 1] = y; + posArray[k + 2] = z; + posArray[k + 3] = 1; + + velArray[k + 0] = vx; + velArray[k + 1] = vy; + velArray[k + 2] = vz; + velArray[k + 3] = mass; + } +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + particleUniforms['cameraConstant'].value = getCameraConstant(camera); +} + +function dynamicValuesChanger() { + velocityUniforms['gravityConstant'].value = effectController.gravityConstant; + velocityUniforms['density'].value = effectController.density; + particleUniforms['density'].value = effectController.density; +} + +function initGUI() { + const gui = new GUI({ width: 280 }); + + const folder1 = gui.addFolder('Dynamic parameters'); + + folder1.add(effectController, 'gravityConstant', 0.0, 1000.0, 0.05).onChange(dynamicValuesChanger); + folder1.add(effectController, 'density', 0.0, 10.0, 0.001).onChange(dynamicValuesChanger); + + const folder2 = gui.addFolder('Static parameters'); + + folder2.add(effectController, 'radius', 10.0, 1000.0, 1.0); + folder2.add(effectController, 'height', 0.0, 50.0, 0.01); + folder2.add(effectController, 'exponent', 0.0, 2.0, 0.001); + folder2.add(effectController, 'maxMass', 1.0, 50.0, 0.1); + folder2.add(effectController, 'velocity', 0.0, 150.0, 0.1); + folder2.add(effectController, 'velocityExponent', 0.0, 1.0, 0.01); + folder2.add(effectController, 'randVelocity', 0.0, 50.0, 0.1); + + const buttonRestart = { + restartSimulation: function () { + restartSimulation(); + }, + }; + + folder2.add(buttonRestart, 'restartSimulation'); + + folder1.open(); + folder2.open(); +} + +function getCameraConstant(camera) { + return window.innerHeight / (Math.tan(THREE.MathUtils.DEG2RAD * 0.5 * camera.fov) / camera.zoom); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + gpuCompute.compute(); + + particleUniforms['texturePosition'].value = gpuCompute.getCurrentRenderTarget(positionVariable).texture; + particleUniforms['textureVelocity'].value = gpuCompute.getCurrentRenderTarget(velocityVariable).texture; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_gpgpu_water.ts b/examples-testing/examples/webgl_gpgpu_water.ts new file mode 100644 index 000000000..00c32f229 --- /dev/null +++ b/examples-testing/examples/webgl_gpgpu_water.ts @@ -0,0 +1,397 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { GPUComputationRenderer } from 'three/addons/misc/GPUComputationRenderer.js'; +import { SimplexNoise } from 'three/addons/math/SimplexNoise.js'; + +// Texture width for simulation +const WIDTH = 128; + +// Water size in system units +const BOUNDS = 512; +const BOUNDS_HALF = BOUNDS * 0.5; + +let container, stats; +let camera, scene, renderer; +let mouseMoved = false; +const mouseCoords = new THREE.Vector2(); +const raycaster = new THREE.Raycaster(); + +let waterMesh; +let meshRay; +let gpuCompute; +let heightmapVariable; +let waterUniforms; +let smoothShader; +let readWaterLevelShader; +let readWaterLevelRenderTarget; +let readWaterLevelImage; +const waterNormal = new THREE.Vector3(); + +const NUM_SPHERES = 5; +const spheres = []; +let spheresEnabled = true; + +const simplex = new SimplexNoise(); + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 3000); + camera.position.set(0, 200, 350); + camera.lookAt(0, 0, 0); + + scene = new THREE.Scene(); + + const sun = new THREE.DirectionalLight(0xffffff, 3.0); + sun.position.set(300, 400, 175); + scene.add(sun); + + const sun2 = new THREE.DirectionalLight(0x40a040, 2.0); + sun2.position.set(-100, 350, -200); + scene.add(sun2); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + container.style.touchAction = 'none'; + container.addEventListener('pointermove', onPointerMove); + + document.addEventListener('keydown', function (event) { + // W Pressed: Toggle wireframe + if (event.keyCode === 87) { + waterMesh.material.wireframe = !waterMesh.material.wireframe; + waterMesh.material.needsUpdate = true; + } + }); + + window.addEventListener('resize', onWindowResize); + + const gui = new GUI(); + + const effectController = { + mouseSize: 20.0, + viscosity: 0.98, + spheresEnabled: spheresEnabled, + }; + + const valuesChanger = function () { + heightmapVariable.material.uniforms['mouseSize'].value = effectController.mouseSize; + heightmapVariable.material.uniforms['viscosityConstant'].value = effectController.viscosity; + spheresEnabled = effectController.spheresEnabled; + for (let i = 0; i < NUM_SPHERES; i++) { + if (spheres[i]) { + spheres[i].visible = spheresEnabled; + } + } + }; + + gui.add(effectController, 'mouseSize', 1.0, 100.0, 1.0).onChange(valuesChanger); + gui.add(effectController, 'viscosity', 0.9, 0.999, 0.001).onChange(valuesChanger); + gui.add(effectController, 'spheresEnabled').onChange(valuesChanger); + const buttonSmooth = { + smoothWater: function () { + smoothWater(); + }, + }; + gui.add(buttonSmooth, 'smoothWater'); + + initWater(); + + createSpheres(); + + valuesChanger(); +} + +function initWater() { + const materialColor = 0x0040c0; + + const geometry = new THREE.PlaneGeometry(BOUNDS, BOUNDS, WIDTH - 1, WIDTH - 1); + + // material: make a THREE.ShaderMaterial clone of THREE.MeshPhongMaterial, with customized vertex shader + const material = new THREE.ShaderMaterial({ + uniforms: THREE.UniformsUtils.merge([ + THREE.ShaderLib['phong'].uniforms, + { + heightmap: { value: null }, + }, + ]), + vertexShader: document.getElementById('waterVertexShader').textContent, + fragmentShader: THREE.ShaderChunk['meshphong_frag'], + }); + + material.lights = true; + + // Material attributes from THREE.MeshPhongMaterial + // Sets the uniforms with the material values + material.uniforms['diffuse'].value = new THREE.Color(materialColor); + material.uniforms['specular'].value = new THREE.Color(0x111111); + material.uniforms['shininess'].value = Math.max(50, 1e-4); + material.uniforms['opacity'].value = material.opacity; + + // Defines + material.defines.WIDTH = WIDTH.toFixed(1); + material.defines.BOUNDS = BOUNDS.toFixed(1); + + waterUniforms = material.uniforms; + + waterMesh = new THREE.Mesh(geometry, material); + waterMesh.rotation.x = -Math.PI / 2; + waterMesh.matrixAutoUpdate = false; + waterMesh.updateMatrix(); + + scene.add(waterMesh); + + // THREE.Mesh just for mouse raycasting + const geometryRay = new THREE.PlaneGeometry(BOUNDS, BOUNDS, 1, 1); + meshRay = new THREE.Mesh(geometryRay, new THREE.MeshBasicMaterial({ color: 0xffffff, visible: false })); + meshRay.rotation.x = -Math.PI / 2; + meshRay.matrixAutoUpdate = false; + meshRay.updateMatrix(); + scene.add(meshRay); + + // Creates the gpu computation class and sets it up + + gpuCompute = new GPUComputationRenderer(WIDTH, WIDTH, renderer); + + const heightmap0 = gpuCompute.createTexture(); + + fillTexture(heightmap0); + + heightmapVariable = gpuCompute.addVariable( + 'heightmap', + document.getElementById('heightmapFragmentShader').textContent, + heightmap0, + ); + + gpuCompute.setVariableDependencies(heightmapVariable, [heightmapVariable]); + + heightmapVariable.material.uniforms['mousePos'] = { value: new THREE.Vector2(10000, 10000) }; + heightmapVariable.material.uniforms['mouseSize'] = { value: 20.0 }; + heightmapVariable.material.uniforms['viscosityConstant'] = { value: 0.98 }; + heightmapVariable.material.uniforms['heightCompensation'] = { value: 0 }; + heightmapVariable.material.defines.BOUNDS = BOUNDS.toFixed(1); + + const error = gpuCompute.init(); + if (error !== null) { + console.error(error); + } + + // Create compute shader to smooth the water surface and velocity + smoothShader = gpuCompute.createShaderMaterial(document.getElementById('smoothFragmentShader').textContent, { + smoothTexture: { value: null }, + }); + + // Create compute shader to read water level + readWaterLevelShader = gpuCompute.createShaderMaterial( + document.getElementById('readWaterLevelFragmentShader').textContent, + { + point1: { value: new THREE.Vector2() }, + levelTexture: { value: null }, + }, + ); + readWaterLevelShader.defines.WIDTH = WIDTH.toFixed(1); + readWaterLevelShader.defines.BOUNDS = BOUNDS.toFixed(1); + + // Create a 4x1 pixel image and a render target (Uint8, 4 channels, 1 byte per channel) to read water height and orientation + readWaterLevelImage = new Uint8Array(4 * 1 * 4); + + readWaterLevelRenderTarget = new THREE.WebGLRenderTarget(4, 1, { + wrapS: THREE.ClampToEdgeWrapping, + wrapT: THREE.ClampToEdgeWrapping, + minFilter: THREE.NearestFilter, + magFilter: THREE.NearestFilter, + format: THREE.RGBAFormat, + type: THREE.UnsignedByteType, + depthBuffer: false, + }); +} + +function fillTexture(texture) { + const waterMaxHeight = 10; + + function noise(x, y) { + let multR = waterMaxHeight; + let mult = 0.025; + let r = 0; + for (let i = 0; i < 15; i++) { + r += multR * simplex.noise(x * mult, y * mult); + multR *= 0.53 + 0.025 * i; + mult *= 1.25; + } + + return r; + } + + const pixels = texture.image.data; + + let p = 0; + for (let j = 0; j < WIDTH; j++) { + for (let i = 0; i < WIDTH; i++) { + const x = (i * 128) / WIDTH; + const y = (j * 128) / WIDTH; + + pixels[p + 0] = noise(x, y); + pixels[p + 1] = pixels[p + 0]; + pixels[p + 2] = 0; + pixels[p + 3] = 1; + + p += 4; + } + } +} + +function smoothWater() { + const currentRenderTarget = gpuCompute.getCurrentRenderTarget(heightmapVariable); + const alternateRenderTarget = gpuCompute.getAlternateRenderTarget(heightmapVariable); + + for (let i = 0; i < 10; i++) { + smoothShader.uniforms['smoothTexture'].value = currentRenderTarget.texture; + gpuCompute.doRenderTarget(smoothShader, alternateRenderTarget); + + smoothShader.uniforms['smoothTexture'].value = alternateRenderTarget.texture; + gpuCompute.doRenderTarget(smoothShader, currentRenderTarget); + } +} + +function createSpheres() { + const sphereTemplate = new THREE.Mesh( + new THREE.SphereGeometry(4, 24, 12), + new THREE.MeshPhongMaterial({ color: 0xffff00 }), + ); + + for (let i = 0; i < NUM_SPHERES; i++) { + let sphere = sphereTemplate; + if (i < NUM_SPHERES - 1) { + sphere = sphereTemplate.clone(); + } + + sphere.position.x = (Math.random() - 0.5) * BOUNDS * 0.7; + sphere.position.z = (Math.random() - 0.5) * BOUNDS * 0.7; + + sphere.userData.velocity = new THREE.Vector3(); + + scene.add(sphere); + + spheres[i] = sphere; + } +} + +function sphereDynamics() { + const currentRenderTarget = gpuCompute.getCurrentRenderTarget(heightmapVariable); + + readWaterLevelShader.uniforms['levelTexture'].value = currentRenderTarget.texture; + + for (let i = 0; i < NUM_SPHERES; i++) { + const sphere = spheres[i]; + + if (sphere) { + // Read water level and orientation + const u = (0.5 * sphere.position.x) / BOUNDS_HALF + 0.5; + const v = 1 - ((0.5 * sphere.position.z) / BOUNDS_HALF + 0.5); + readWaterLevelShader.uniforms['point1'].value.set(u, v); + gpuCompute.doRenderTarget(readWaterLevelShader, readWaterLevelRenderTarget); + + renderer.readRenderTargetPixels(readWaterLevelRenderTarget, 0, 0, 4, 1, readWaterLevelImage); + const pixels = new Float32Array(readWaterLevelImage.buffer); + + // Get orientation + waterNormal.set(pixels[1], 0, -pixels[2]); + + const pos = sphere.position; + + // Set height + pos.y = pixels[0]; + + // Move sphere + waterNormal.multiplyScalar(0.1); + sphere.userData.velocity.add(waterNormal); + sphere.userData.velocity.multiplyScalar(0.998); + pos.add(sphere.userData.velocity); + + if (pos.x < -BOUNDS_HALF) { + pos.x = -BOUNDS_HALF + 0.001; + sphere.userData.velocity.x *= -0.3; + } else if (pos.x > BOUNDS_HALF) { + pos.x = BOUNDS_HALF - 0.001; + sphere.userData.velocity.x *= -0.3; + } + + if (pos.z < -BOUNDS_HALF) { + pos.z = -BOUNDS_HALF + 0.001; + sphere.userData.velocity.z *= -0.3; + } else if (pos.z > BOUNDS_HALF) { + pos.z = BOUNDS_HALF - 0.001; + sphere.userData.velocity.z *= -0.3; + } + } + } +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function setMouseCoords(x, y) { + mouseCoords.set((x / renderer.domElement.clientWidth) * 2 - 1, -(y / renderer.domElement.clientHeight) * 2 + 1); + mouseMoved = true; +} + +function onPointerMove(event) { + if (event.isPrimary === false) return; + + setMouseCoords(event.clientX, event.clientY); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + // Set uniforms: mouse interaction + const uniforms = heightmapVariable.material.uniforms; + if (mouseMoved) { + raycaster.setFromCamera(mouseCoords, camera); + + const intersects = raycaster.intersectObject(meshRay); + + if (intersects.length > 0) { + const point = intersects[0].point; + uniforms['mousePos'].value.set(point.x, point.z); + } else { + uniforms['mousePos'].value.set(10000, 10000); + } + + mouseMoved = false; + } else { + uniforms['mousePos'].value.set(10000, 10000); + } + + // Do the gpu computation + gpuCompute.compute(); + + if (spheresEnabled) { + sphereDynamics(); + } + + // Get compute output in custom uniform + waterUniforms['heightmap'].value = gpuCompute.getCurrentRenderTarget(heightmapVariable).texture; + + // Render + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_helpers.ts b/examples-testing/examples/webgl_helpers.ts new file mode 100644 index 000000000..a8c3b9773 --- /dev/null +++ b/examples-testing/examples/webgl_helpers.ts @@ -0,0 +1,117 @@ +import * as THREE from 'three'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +import { VertexNormalsHelper } from 'three/addons/helpers/VertexNormalsHelper.js'; +import { VertexTangentsHelper } from 'three/addons/helpers/VertexTangentsHelper.js'; + +let scene, renderer; +let camera, light; +let vnh; +let vth; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 400; + + scene = new THREE.Scene(); + + light = new THREE.PointLight(); + light.position.set(200, 100, 150); + scene.add(light); + + scene.add(new THREE.PointLightHelper(light, 15)); + + const gridHelper = new THREE.GridHelper(400, 40, 0x0000ff, 0x808080); + gridHelper.position.y = -150; + gridHelper.position.x = -150; + scene.add(gridHelper); + + const polarGridHelper = new THREE.PolarGridHelper(200, 16, 8, 64, 0x0000ff, 0x808080); + polarGridHelper.position.y = -150; + polarGridHelper.position.x = 200; + scene.add(polarGridHelper); + + const loader = new GLTFLoader(); + loader.load('models/gltf/LeePerrySmith/LeePerrySmith.glb', function (gltf) { + const mesh = gltf.scene.children[0]; + + mesh.geometry.computeTangents(); // generates bad data due to degenerate UVs + + const group = new THREE.Group(); + group.scale.multiplyScalar(50); + scene.add(group); + + // To make sure that the matrixWorld is up to date for the boxhelpers + group.updateMatrixWorld(true); + + group.add(mesh); + + vnh = new VertexNormalsHelper(mesh, 5); + scene.add(vnh); + + vth = new VertexTangentsHelper(mesh, 5); + scene.add(vth); + + scene.add(new THREE.BoxHelper(mesh)); + + const wireframe = new THREE.WireframeGeometry(mesh.geometry); + let line = new THREE.LineSegments(wireframe); + line.material.depthTest = false; + line.material.opacity = 0.25; + line.material.transparent = true; + line.position.x = 4; + group.add(line); + scene.add(new THREE.BoxHelper(line)); + + const edges = new THREE.EdgesGeometry(mesh.geometry); + line = new THREE.LineSegments(edges); + line.material.depthTest = false; + line.material.opacity = 0.25; + line.material.transparent = true; + line.position.x = -4; + group.add(line); + scene.add(new THREE.BoxHelper(line)); + + scene.add(new THREE.BoxHelper(group)); + scene.add(new THREE.BoxHelper(scene)); + }); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const time = -performance.now() * 0.0003; + + camera.position.x = 400 * Math.cos(time); + camera.position.z = 400 * Math.sin(time); + camera.lookAt(scene.position); + + light.position.x = Math.sin(time * 1.7) * 300; + light.position.y = Math.cos(time * 1.5) * 400; + light.position.z = Math.cos(time * 1.3) * 300; + + if (vnh) vnh.update(); + if (vth) vth.update(); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_instancing_dynamic.ts b/examples-testing/examples/webgl_instancing_dynamic.ts new file mode 100644 index 000000000..88562fc5a --- /dev/null +++ b/examples-testing/examples/webgl_instancing_dynamic.ts @@ -0,0 +1,103 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, stats; + +let mesh; +const amount = parseInt(window.location.search.slice(1)) || 10; +const count = Math.pow(amount, 3); +const dummy = new THREE.Object3D(); + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(amount * 0.9, amount * 0.9, amount * 0.9); + camera.lookAt(0, 0, 0); + + scene = new THREE.Scene(); + + const loader = new THREE.BufferGeometryLoader(); + loader.load('models/json/suzanne_buffergeometry.json', function (geometry) { + geometry.computeVertexNormals(); + geometry.scale(0.5, 0.5, 0.5); + + const material = new THREE.MeshNormalMaterial(); + // check overdraw + // let material = new THREE.MeshBasicMaterial( { color: 0xff0000, opacity: 0.1, transparent: true } ); + + mesh = new THREE.InstancedMesh(geometry, material, count); + mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage); // will be updated every frame + scene.add(mesh); + + // + + const gui = new GUI(); + gui.add(mesh, 'count', 0, count); + }); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + render(); + + stats.update(); +} + +function render() { + if (mesh) { + const time = Date.now() * 0.001; + + mesh.rotation.x = Math.sin(time / 4); + mesh.rotation.y = Math.sin(time / 2); + + let i = 0; + const offset = (amount - 1) / 2; + + for (let x = 0; x < amount; x++) { + for (let y = 0; y < amount; y++) { + for (let z = 0; z < amount; z++) { + dummy.position.set(offset - x, offset - y, offset - z); + dummy.rotation.y = Math.sin(x / 4 + time) + Math.sin(y / 4 + time) + Math.sin(z / 4 + time); + dummy.rotation.z = dummy.rotation.y * 2; + + dummy.updateMatrix(); + + mesh.setMatrixAt(i++, dummy.matrix); + } + } + } + + mesh.instanceMatrix.needsUpdate = true; + mesh.computeBoundingSphere(); + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_instancing_morph.ts b/examples-testing/examples/webgl_instancing_morph.ts new file mode 100644 index 000000000..8686a75b9 --- /dev/null +++ b/examples-testing/examples/webgl_instancing_morph.ts @@ -0,0 +1,147 @@ +import * as THREE from 'three'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let camera, scene, renderer, stats, mesh, mixer, dummy; + +const offset = 5000; + +const timeOffsets = new Float32Array(1024); + +for (let i = 0; i < 1024; i++) { + timeOffsets[i] = Math.random() * 3; +} + +const clock = new THREE.Clock(true); + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 100, 10000); + + scene = new THREE.Scene(); + + scene.background = new THREE.Color(0x99ddff); + + scene.fog = new THREE.Fog(0x99ddff, 5000, 10000); + + const light = new THREE.DirectionalLight(0xffffff, 1); + + light.position.set(200, 1000, 50); + + light.castShadow = true; + + light.shadow.camera.left = -5000; + light.shadow.camera.right = 5000; + light.shadow.camera.top = 5000; + light.shadow.camera.bottom = -5000; + light.shadow.camera.far = 2000; + + light.shadow.bias = -0.01; + + light.shadow.camera.updateProjectionMatrix(); + + scene.add(light); + + const hemi = new THREE.HemisphereLight(0x99ddff, 0x669933, 1 / 3); + + scene.add(hemi); + + const ground = new THREE.Mesh( + new THREE.PlaneGeometry(1000000, 1000000), + new THREE.MeshStandardMaterial({ color: 0x669933, depthWrite: true }), + ); + + ground.rotation.x = -Math.PI / 2; + + ground.receiveShadow = true; + + scene.add(ground); + + const loader = new GLTFLoader(); + + loader.load('models/gltf/Horse.glb', function (glb) { + dummy = glb.scene.children[0]; + + mesh = new THREE.InstancedMesh(dummy.geometry, dummy.material, 1024); + + mesh.castShadow = true; + + for (let x = 0, i = 0; x < 32; x++) { + for (let y = 0; y < 32; y++) { + dummy.position.set(offset - 300 * x + 200 * Math.random(), 0, offset - 300 * y); + + dummy.updateMatrix(); + + mesh.setMatrixAt(i, dummy.matrix); + + mesh.setColorAt(i, new THREE.Color(`hsl(${Math.random() * 360}, 50%, 66%)`)); + + i++; + } + } + + scene.add(mesh); + + mixer = new THREE.AnimationMixer(glb.scene); + + const action = mixer.clipAction(glb.animations[0]); + + action.play(); + }); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.VSMShadowMap; + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + render(); + + stats.update(); +} + +function render() { + const time = clock.getElapsedTime(); + + const r = 3000; + camera.position.set(Math.sin(time / 10) * r, 1500 + 1000 * Math.cos(time / 5), Math.cos(time / 10) * r); + camera.lookAt(0, 0, 0); + + if (mesh) { + for (let i = 0; i < 1024; i++) { + mixer.setTime(time + timeOffsets[i]); + + mesh.setMorphAt(i, dummy); + } + + mesh.morphTexture.needsUpdate = true; + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_instancing_performance.ts b/examples-testing/examples/webgl_instancing_performance.ts new file mode 100644 index 000000000..bf1deabad --- /dev/null +++ b/examples-testing/examples/webgl_instancing_performance.ts @@ -0,0 +1,262 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; + +let container, stats, gui, guiStatsEl; +let camera, controls, scene, renderer, material; + +// gui + +const Method = { + INSTANCED: 'INSTANCED', + MERGED: 'MERGED', + NAIVE: 'NAIVE', +}; + +const api = { + method: Method.INSTANCED, + count: 1000, +}; + +// + +init(); +initMesh(); + +// + +function clean() { + const meshes = []; + + scene.traverse(function (object) { + if (object.isMesh) meshes.push(object); + }); + + for (let i = 0; i < meshes.length; i++) { + const mesh = meshes[i]; + mesh.material.dispose(); + mesh.geometry.dispose(); + + scene.remove(mesh); + } +} + +const randomizeMatrix = (function () { + const position = new THREE.Vector3(); + const quaternion = new THREE.Quaternion(); + const scale = new THREE.Vector3(); + + return function (matrix) { + position.x = Math.random() * 40 - 20; + position.y = Math.random() * 40 - 20; + position.z = Math.random() * 40 - 20; + + quaternion.random(); + + scale.x = scale.y = scale.z = Math.random() * 1; + + matrix.compose(position, quaternion, scale); + }; +})(); + +function initMesh() { + clean(); + + // make instances + new THREE.BufferGeometryLoader().setPath('models/json/').load('suzanne_buffergeometry.json', function (geometry) { + material = new THREE.MeshNormalMaterial(); + + geometry.computeVertexNormals(); + + console.time(api.method + ' (build)'); + + switch (api.method) { + case Method.INSTANCED: + makeInstanced(geometry); + break; + + case Method.MERGED: + makeMerged(geometry); + break; + + case Method.NAIVE: + makeNaive(geometry); + break; + } + + console.timeEnd(api.method + ' (build)'); + }); +} + +function makeInstanced(geometry) { + const matrix = new THREE.Matrix4(); + const mesh = new THREE.InstancedMesh(geometry, material, api.count); + + for (let i = 0; i < api.count; i++) { + randomizeMatrix(matrix); + mesh.setMatrixAt(i, matrix); + } + + scene.add(mesh); + + // + + const geometryByteLength = getGeometryByteLength(geometry); + + guiStatsEl.innerHTML = [ + 'GPU draw calls: 1', + 'GPU memory: ' + formatBytes(api.count * 16 + geometryByteLength, 2), + ].join('
'); +} + +function makeMerged(geometry) { + const geometries = []; + const matrix = new THREE.Matrix4(); + + for (let i = 0; i < api.count; i++) { + randomizeMatrix(matrix); + + const instanceGeometry = geometry.clone(); + instanceGeometry.applyMatrix4(matrix); + + geometries.push(instanceGeometry); + } + + const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries); + + scene.add(new THREE.Mesh(mergedGeometry, material)); + + // + + guiStatsEl.innerHTML = [ + 'GPU draw calls: 1', + 'GPU memory: ' + formatBytes(getGeometryByteLength(mergedGeometry), 2), + ].join('
'); +} + +function makeNaive(geometry) { + const matrix = new THREE.Matrix4(); + + for (let i = 0; i < api.count; i++) { + randomizeMatrix(matrix); + + const mesh = new THREE.Mesh(geometry, material); + mesh.applyMatrix4(matrix); + + scene.add(mesh); + } + + // + + const geometryByteLength = getGeometryByteLength(geometry); + + guiStatsEl.innerHTML = [ + 'GPU draw calls: ' + api.count, + 'GPU memory: ' + formatBytes(api.count * 16 + geometryByteLength, 2), + ].join('
'); +} + +function init() { + const width = window.innerWidth; + const height = window.innerHeight; + + // camera + + camera = new THREE.PerspectiveCamera(70, width / height, 1, 100); + camera.position.z = 30; + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(width, height); + renderer.setAnimationLoop(animate); + container = document.getElementById('container'); + container.appendChild(renderer.domElement); + + // scene + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + + // controls + + controls = new OrbitControls(camera, renderer.domElement); + controls.autoRotate = true; + + // stats + + stats = new Stats(); + container.appendChild(stats.dom); + + // gui + + gui = new GUI(); + gui.add(api, 'method', Method).onChange(initMesh); + gui.add(api, 'count', 1, 10000).step(1).onChange(initMesh); + + const perfFolder = gui.addFolder('Performance'); + + guiStatsEl = document.createElement('div'); + guiStatsEl.classList.add('gui-stats'); + + perfFolder.$children.appendChild(guiStatsEl); + perfFolder.open(); + + // listeners + + window.addEventListener('resize', onWindowResize); + + Object.assign(window, { scene }); +} + +// + +function onWindowResize() { + 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); + + stats.update(); +} + +// + +function getGeometryByteLength(geometry) { + let total = 0; + + if (geometry.index) total += geometry.index.array.byteLength; + + for (const name in geometry.attributes) { + total += geometry.attributes[name].array.byteLength; + } + + return total; +} + +// Source: https://stackoverflow.com/a/18650828/1314762 +function formatBytes(bytes, decimals) { + if (bytes === 0) return '0 bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['bytes', 'KB', 'MB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} diff --git a/examples-testing/examples/webgl_instancing_raycast.ts b/examples-testing/examples/webgl_instancing_raycast.ts new file mode 100644 index 000000000..371ea070b --- /dev/null +++ b/examples-testing/examples/webgl_instancing_raycast.ts @@ -0,0 +1,116 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer, controls, stats; + +let mesh; +const amount = parseInt(window.location.search.slice(1)) || 10; +const count = Math.pow(amount, 3); + +const raycaster = new THREE.Raycaster(); +const mouse = new THREE.Vector2(1, 1); + +const color = new THREE.Color(); +const white = new THREE.Color().setHex(0xffffff); + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(amount, amount, amount); + camera.lookAt(0, 0, 0); + + scene = new THREE.Scene(); + + const light = new THREE.HemisphereLight(0xffffff, 0x888888, 3); + light.position.set(0, 1, 0); + scene.add(light); + + const geometry = new THREE.IcosahedronGeometry(0.5, 3); + const material = new THREE.MeshPhongMaterial({ color: 0xffffff }); + + mesh = new THREE.InstancedMesh(geometry, material, count); + + let i = 0; + const offset = (amount - 1) / 2; + + const matrix = new THREE.Matrix4(); + + for (let x = 0; x < amount; x++) { + for (let y = 0; y < amount; y++) { + for (let z = 0; z < amount; z++) { + matrix.setPosition(offset - x, offset - y, offset - z); + + mesh.setMatrixAt(i, matrix); + mesh.setColorAt(i, color); + + i++; + } + } + } + + scene.add(mesh); + + // + + const gui = new GUI(); + gui.add(mesh, 'count', 0, count); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.enableZoom = false; + controls.enablePan = false; + + stats = new Stats(); + document.body.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); + document.addEventListener('mousemove', onMouseMove); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onMouseMove(event) { + event.preventDefault(); + + mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; +} + +function animate() { + controls.update(); + + raycaster.setFromCamera(mouse, camera); + + const intersection = raycaster.intersectObject(mesh); + + if (intersection.length > 0) { + const instanceId = intersection[0].instanceId; + + mesh.getColorAt(instanceId, color); + + if (color.equals(white)) { + mesh.setColorAt(instanceId, color.setHex(Math.random() * 0xffffff)); + + mesh.instanceColor.needsUpdate = true; + } + } + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_instancing_scatter.ts b/examples-testing/examples/webgl_instancing_scatter.ts new file mode 100644 index 000000000..fc3b9cc9f --- /dev/null +++ b/examples-testing/examples/webgl_instancing_scatter.ts @@ -0,0 +1,257 @@ +import * as THREE from 'three'; + +import { MeshSurfaceSampler } from 'three/addons/math/MeshSurfaceSampler.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, stats; + +const api = { + count: 2000, + distribution: 'random', + resample: resample, + surfaceColor: 0xfff784, + backgroundColor: 0xe39469, +}; + +let stemMesh, blossomMesh; +let stemGeometry, blossomGeometry; +let stemMaterial, blossomMaterial; + +let sampler; +const count = api.count; +const ages = new Float32Array(count); +const scales = new Float32Array(count); +const dummy = new THREE.Object3D(); + +const _position = new THREE.Vector3(); +const _normal = new THREE.Vector3(); +const _scale = new THREE.Vector3(); + +// let surfaceGeometry = new THREE.BoxGeometry( 10, 10, 10 ).toNonIndexed(); +const surfaceGeometry = new THREE.TorusKnotGeometry(10, 3, 100, 16).toNonIndexed(); +const surfaceMaterial = new THREE.MeshLambertMaterial({ color: api.surfaceColor, wireframe: false }); +const surface = new THREE.Mesh(surfaceGeometry, surfaceMaterial); + +// Source: https://gist.github.com/gre/1650294 +const easeOutCubic = function (t) { + return --t * t * t + 1; +}; + +// Scaling curve causes particles to grow quickly, ease gradually into full scale, then +// disappear quickly. More of the particle's lifetime is spent around full scale. +const scaleCurve = function (t) { + return Math.abs(easeOutCubic((t > 0.5 ? 1 - t : t) * 2)); +}; + +const loader = new GLTFLoader(); + +loader.load('./models/gltf/Flower/Flower.glb', function (gltf) { + const _stemMesh = gltf.scene.getObjectByName('Stem'); + const _blossomMesh = gltf.scene.getObjectByName('Blossom'); + + stemGeometry = _stemMesh.geometry.clone(); + blossomGeometry = _blossomMesh.geometry.clone(); + + const defaultTransform = new THREE.Matrix4() + .makeRotationX(Math.PI) + .multiply(new THREE.Matrix4().makeScale(7, 7, 7)); + + stemGeometry.applyMatrix4(defaultTransform); + blossomGeometry.applyMatrix4(defaultTransform); + + stemMaterial = _stemMesh.material; + blossomMaterial = _blossomMesh.material; + + stemMesh = new THREE.InstancedMesh(stemGeometry, stemMaterial, count); + blossomMesh = new THREE.InstancedMesh(blossomGeometry, blossomMaterial, count); + + // Assign random colors to the blossoms. + const color = new THREE.Color(); + const blossomPalette = [0xf20587, 0xf2d479, 0xf2c879, 0xf2b077, 0xf24405]; + + for (let i = 0; i < count; i++) { + color.setHex(blossomPalette[Math.floor(Math.random() * blossomPalette.length)]); + blossomMesh.setColorAt(i, color); + } + + // Instance matrices will be updated every frame. + stemMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage); + blossomMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage); + + resample(); + + init(); +}); + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(25, 25, 25); + camera.lookAt(0, 0, 0); + + // + + scene = new THREE.Scene(); + scene.background = new THREE.Color(api.backgroundColor); + + const pointLight = new THREE.PointLight(0xaa8899, 2.5, 0, 0); + pointLight.position.set(50, -25, 75); + scene.add(pointLight); + + scene.add(new THREE.AmbientLight(0xffffff, 3)); + + // + + scene.add(stemMesh); + scene.add(blossomMesh); + + scene.add(surface); + + // + + const gui = new GUI(); + gui.add(api, 'count', 0, count).onChange(function () { + stemMesh.count = api.count; + blossomMesh.count = api.count; + }); + + // gui.addColor( api, 'backgroundColor' ).onChange( function () { + + // scene.background.setHex( api.backgroundColor ); + + // } ); + + // gui.addColor( api, 'surfaceColor' ).onChange( function () { + + // surfaceMaterial.color.setHex( api.surfaceColor ); + + // } ); + + gui.add(api, 'distribution').options(['random', 'weighted']).onChange(resample); + gui.add(api, 'resample'); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function resample() { + const vertexCount = surface.geometry.getAttribute('position').count; + + console.info('Sampling ' + count + ' points from a surface with ' + vertexCount + ' vertices...'); + + // + + console.time('.build()'); + + sampler = new MeshSurfaceSampler(surface).setWeightAttribute(api.distribution === 'weighted' ? 'uv' : null).build(); + + console.timeEnd('.build()'); + + // + + console.time('.sample()'); + + for (let i = 0; i < count; i++) { + ages[i] = Math.random(); + scales[i] = scaleCurve(ages[i]); + + resampleParticle(i); + } + + console.timeEnd('.sample()'); + + stemMesh.instanceMatrix.needsUpdate = true; + blossomMesh.instanceMatrix.needsUpdate = true; +} + +function resampleParticle(i) { + sampler.sample(_position, _normal); + _normal.add(_position); + + dummy.position.copy(_position); + dummy.scale.set(scales[i], scales[i], scales[i]); + dummy.lookAt(_normal); + dummy.updateMatrix(); + + stemMesh.setMatrixAt(i, dummy.matrix); + blossomMesh.setMatrixAt(i, dummy.matrix); +} + +function updateParticle(i) { + // Update lifecycle. + + ages[i] += 0.005; + + if (ages[i] >= 1) { + ages[i] = 0.001; + scales[i] = scaleCurve(ages[i]); + + resampleParticle(i); + + return; + } + + // Update scale. + + const prevScale = scales[i]; + scales[i] = scaleCurve(ages[i]); + _scale.set(scales[i] / prevScale, scales[i] / prevScale, scales[i] / prevScale); + + // Update transform. + + stemMesh.getMatrixAt(i, dummy.matrix); + dummy.matrix.scale(_scale); + stemMesh.setMatrixAt(i, dummy.matrix); + blossomMesh.setMatrixAt(i, dummy.matrix); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + render(); + + stats.update(); +} + +function render() { + if (stemMesh && blossomMesh) { + const time = Date.now() * 0.001; + + scene.rotation.x = Math.sin(time / 4); + scene.rotation.y = Math.sin(time / 2); + + for (let i = 0; i < api.count; i++) { + updateParticle(i); + } + + stemMesh.instanceMatrix.needsUpdate = true; + blossomMesh.instanceMatrix.needsUpdate = true; + + stemMesh.computeBoundingSphere(); + blossomMesh.computeBoundingSphere(); + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_interactive_buffergeometry.ts b/examples-testing/examples/webgl_interactive_buffergeometry.ts new file mode 100644 index 000000000..1d6608b13 --- /dev/null +++ b/examples-testing/examples/webgl_interactive_buffergeometry.ts @@ -0,0 +1,244 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; + +let camera, scene, renderer; + +let raycaster, pointer; + +let mesh, line; + +init(); + +function init() { + container = document.getElementById('container'); + + // + + camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 1, 3500); + camera.position.z = 2750; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050505); + scene.fog = new THREE.Fog(0x050505, 2000, 3500); + + // + + scene.add(new THREE.AmbientLight(0x444444, 3)); + + const light1 = new THREE.DirectionalLight(0xffffff, 1.5); + light1.position.set(1, 1, 1); + scene.add(light1); + + const light2 = new THREE.DirectionalLight(0xffffff, 4.5); + light2.position.set(0, -1, 0); + scene.add(light2); + + // + + const triangles = 5000; + + let geometry = new THREE.BufferGeometry(); + + const positions = new Float32Array(triangles * 3 * 3); + const normals = new Float32Array(triangles * 3 * 3); + const colors = new Float32Array(triangles * 3 * 3); + + const color = new THREE.Color(); + + const n = 800, + n2 = n / 2; // triangles spread in the cube + const d = 120, + d2 = d / 2; // individual triangle size + + const pA = new THREE.Vector3(); + const pB = new THREE.Vector3(); + const pC = new THREE.Vector3(); + + const cb = new THREE.Vector3(); + const ab = new THREE.Vector3(); + + for (let i = 0; i < positions.length; i += 9) { + // positions + + const x = Math.random() * n - n2; + const y = Math.random() * n - n2; + const z = Math.random() * n - n2; + + const ax = x + Math.random() * d - d2; + const ay = y + Math.random() * d - d2; + const az = z + Math.random() * d - d2; + + const bx = x + Math.random() * d - d2; + const by = y + Math.random() * d - d2; + const bz = z + Math.random() * d - d2; + + const cx = x + Math.random() * d - d2; + const cy = y + Math.random() * d - d2; + const cz = z + Math.random() * d - d2; + + positions[i] = ax; + positions[i + 1] = ay; + positions[i + 2] = az; + + positions[i + 3] = bx; + positions[i + 4] = by; + positions[i + 5] = bz; + + positions[i + 6] = cx; + positions[i + 7] = cy; + positions[i + 8] = cz; + + // flat face normals + + pA.set(ax, ay, az); + pB.set(bx, by, bz); + pC.set(cx, cy, cz); + + cb.subVectors(pC, pB); + ab.subVectors(pA, pB); + cb.cross(ab); + + cb.normalize(); + + const nx = cb.x; + const ny = cb.y; + const nz = cb.z; + + normals[i] = nx; + normals[i + 1] = ny; + normals[i + 2] = nz; + + normals[i + 3] = nx; + normals[i + 4] = ny; + normals[i + 5] = nz; + + normals[i + 6] = nx; + normals[i + 7] = ny; + normals[i + 8] = nz; + + // colors + + const vx = x / n + 0.5; + const vy = y / n + 0.5; + const vz = z / n + 0.5; + + color.setRGB(vx, vy, vz); + + colors[i] = color.r; + colors[i + 1] = color.g; + colors[i + 2] = color.b; + + colors[i + 3] = color.r; + colors[i + 4] = color.g; + colors[i + 5] = color.b; + + colors[i + 6] = color.r; + colors[i + 7] = color.g; + colors[i + 8] = color.b; + } + + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3)); + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); + + geometry.computeBoundingSphere(); + + let material = new THREE.MeshPhongMaterial({ + color: 0xaaaaaa, + specular: 0xffffff, + shininess: 250, + side: THREE.DoubleSide, + vertexColors: true, + }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + raycaster = new THREE.Raycaster(); + + pointer = new THREE.Vector2(); + + geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(4 * 3), 3)); + + material = new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true }); + + line = new THREE.Line(geometry, material); + scene.add(line); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); + document.addEventListener('pointermove', onPointerMove); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onPointerMove(event) { + pointer.x = (event.clientX / window.innerWidth) * 2 - 1; + pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + const time = Date.now() * 0.001; + + mesh.rotation.x = time * 0.15; + mesh.rotation.y = time * 0.25; + + raycaster.setFromCamera(pointer, camera); + + const intersects = raycaster.intersectObject(mesh); + + if (intersects.length > 0) { + const intersect = intersects[0]; + const face = intersect.face; + + const linePosition = line.geometry.attributes.position; + const meshPosition = mesh.geometry.attributes.position; + + linePosition.copyAt(0, meshPosition, face.a); + linePosition.copyAt(1, meshPosition, face.b); + linePosition.copyAt(2, meshPosition, face.c); + linePosition.copyAt(3, meshPosition, face.a); + + mesh.updateMatrix(); + + line.geometry.applyMatrix4(mesh.matrix); + + line.visible = true; + } else { + line.visible = false; + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_interactive_cubes.ts b/examples-testing/examples/webgl_interactive_cubes.ts new file mode 100644 index 000000000..adfcfddf8 --- /dev/null +++ b/examples-testing/examples/webgl_interactive_cubes.ts @@ -0,0 +1,114 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let stats; +let camera, scene, raycaster, renderer; + +let INTERSECTED; +let theta = 0; + +const pointer = new THREE.Vector2(); +const radius = 5; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(1, 1, 1).normalize(); + scene.add(light); + + const geometry = new THREE.BoxGeometry(); + + for (let i = 0; i < 2000; i++) { + const object = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ color: Math.random() * 0xffffff })); + + object.position.x = Math.random() * 40 - 20; + object.position.y = Math.random() * 40 - 20; + object.position.z = Math.random() * 40 - 20; + + object.rotation.x = Math.random() * 2 * Math.PI; + object.rotation.y = Math.random() * 2 * Math.PI; + object.rotation.z = Math.random() * 2 * Math.PI; + + object.scale.x = Math.random() + 0.5; + object.scale.y = Math.random() + 0.5; + object.scale.z = Math.random() + 0.5; + + scene.add(object); + } + + raycaster = new THREE.Raycaster(); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + document.addEventListener('mousemove', onPointerMove); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onPointerMove(event) { + pointer.x = (event.clientX / window.innerWidth) * 2 - 1; + pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + theta += 0.1; + + camera.position.x = radius * Math.sin(THREE.MathUtils.degToRad(theta)); + camera.position.y = radius * Math.sin(THREE.MathUtils.degToRad(theta)); + camera.position.z = radius * Math.cos(THREE.MathUtils.degToRad(theta)); + camera.lookAt(scene.position); + + camera.updateMatrixWorld(); + + // find intersections + + raycaster.setFromCamera(pointer, camera); + + const intersects = raycaster.intersectObjects(scene.children, false); + + if (intersects.length > 0) { + if (INTERSECTED != intersects[0].object) { + if (INTERSECTED) INTERSECTED.material.emissive.setHex(INTERSECTED.currentHex); + + INTERSECTED = intersects[0].object; + INTERSECTED.currentHex = INTERSECTED.material.emissive.getHex(); + INTERSECTED.material.emissive.setHex(0xff0000); + } + } else { + if (INTERSECTED) INTERSECTED.material.emissive.setHex(INTERSECTED.currentHex); + + INTERSECTED = null; + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_interactive_cubes_gpu.ts b/examples-testing/examples/webgl_interactive_cubes_gpu.ts new file mode 100644 index 000000000..2644469c3 --- /dev/null +++ b/examples-testing/examples/webgl_interactive_cubes_gpu.ts @@ -0,0 +1,229 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { TrackballControls } from 'three/addons/controls/TrackballControls.js'; +import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; + +let container, stats; +let camera, controls, scene, renderer; +let pickingTexture, pickingScene; +let highlightBox; + +const pickingData = []; + +const pointer = new THREE.Vector2(); +const offset = new THREE.Vector3(10, 10, 10); +const clearColor = new THREE.Color(); + +init(); + +function init() { + container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 1000; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + + scene.add(new THREE.AmbientLight(0xcccccc)); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0, 500, 2000); + scene.add(light); + + const defaultMaterial = new THREE.MeshPhongMaterial({ + color: 0xffffff, + flatShading: true, + vertexColors: true, + shininess: 0, + }); + + // set up the picking texture to use a 32 bit integer so we can write and read integer ids from it + pickingScene = new THREE.Scene(); + pickingTexture = new THREE.WebGLRenderTarget(1, 1, { + type: THREE.IntType, + format: THREE.RGBAIntegerFormat, + internalFormat: 'RGBA32I', + }); + const pickingMaterial = new THREE.ShaderMaterial({ + glslVersion: THREE.GLSL3, + + vertexShader: /* glsl */ ` + attribute int id; + flat varying int vid; + void main() { + + vid = id; + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); + + } + `, + + fragmentShader: /* glsl */ ` + layout(location = 0) out int out_id; + flat varying int vid; + + void main() { + + out_id = vid; + + } + `, + }); + + function applyId(geometry, id) { + const position = geometry.attributes.position; + const array = new Int16Array(position.count); + array.fill(id); + + const bufferAttribute = new THREE.Int16BufferAttribute(array, 1, false); + bufferAttribute.gpuType = THREE.IntType; + geometry.setAttribute('id', bufferAttribute); + } + + function applyVertexColors(geometry, color) { + const position = geometry.attributes.position; + const colors = []; + + for (let i = 0; i < position.count; i++) { + colors.push(color.r, color.g, color.b); + } + + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + } + + const geometries = []; + const matrix = new THREE.Matrix4(); + const quaternion = new THREE.Quaternion(); + const color = new THREE.Color(); + + for (let i = 0; i < 5000; i++) { + const geometry = new THREE.BoxGeometry(); + + const position = new THREE.Vector3(); + position.x = Math.random() * 10000 - 5000; + position.y = Math.random() * 6000 - 3000; + position.z = Math.random() * 8000 - 4000; + + const rotation = new THREE.Euler(); + rotation.x = Math.random() * 2 * Math.PI; + rotation.y = Math.random() * 2 * Math.PI; + rotation.z = Math.random() * 2 * Math.PI; + + const scale = new THREE.Vector3(); + scale.x = Math.random() * 200 + 100; + scale.y = Math.random() * 200 + 100; + scale.z = Math.random() * 200 + 100; + + quaternion.setFromEuler(rotation); + matrix.compose(position, quaternion, scale); + + geometry.applyMatrix4(matrix); + + // give the geometry's vertices a random color to be displayed and an integer + // identifier as a vertex attribute so boxes can be identified after being merged. + applyVertexColors(geometry, color.setHex(Math.random() * 0xffffff)); + applyId(geometry, i); + + geometries.push(geometry); + + pickingData[i] = { + position: position, + rotation: rotation, + scale: scale, + }; + } + + const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries); + scene.add(new THREE.Mesh(mergedGeometry, defaultMaterial)); + pickingScene.add(new THREE.Mesh(mergedGeometry, pickingMaterial)); + + highlightBox = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshLambertMaterial({ color: 0xffff00 })); + scene.add(highlightBox); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + controls = new TrackballControls(camera, renderer.domElement); + controls.rotateSpeed = 1.0; + controls.zoomSpeed = 1.2; + controls.panSpeed = 0.8; + controls.noZoom = false; + controls.noPan = false; + controls.staticMoving = true; + controls.dynamicDampingFactor = 0.3; + + stats = new Stats(); + container.appendChild(stats.dom); + + renderer.domElement.addEventListener('pointermove', onPointerMove); +} + +// + +function onPointerMove(e) { + pointer.x = e.clientX; + pointer.y = e.clientY; +} + +function animate() { + render(); + stats.update(); +} + +function pick() { + // render the picking scene off-screen + // set the view offset to represent just a single pixel under the mouse + const dpr = window.devicePixelRatio; + camera.setViewOffset( + renderer.domElement.width, + renderer.domElement.height, + Math.floor(pointer.x * dpr), + Math.floor(pointer.y * dpr), + 1, + 1, + ); + + // render the scene + renderer.setRenderTarget(pickingTexture); + + // clear the background to - 1 meaning no item was hit + clearColor.setRGB(-1, -1, -1); + renderer.setClearColor(clearColor); + renderer.render(pickingScene, camera); + + // clear the view offset so rendering returns to normal + camera.clearViewOffset(); + + // create buffer for reading single pixel + const pixelBuffer = new Int32Array(4); + + // read the pixel + renderer.readRenderTargetPixelsAsync(pickingTexture, 0, 0, 1, 1, pixelBuffer).then(() => { + const id = pixelBuffer[0]; + if (id !== -1) { + // move our highlightBox so that it surrounds the picked object + const data = pickingData[id]; + highlightBox.position.copy(data.position); + highlightBox.rotation.copy(data.rotation); + highlightBox.scale.copy(data.scale).add(offset); + highlightBox.visible = true; + } else { + highlightBox.visible = false; + } + }); +} + +function render() { + controls.update(); + + pick(); + + renderer.setRenderTarget(null); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_interactive_cubes_ortho.ts b/examples-testing/examples/webgl_interactive_cubes_ortho.ts new file mode 100644 index 000000000..520674b5f --- /dev/null +++ b/examples-testing/examples/webgl_interactive_cubes_ortho.ts @@ -0,0 +1,129 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let stats; +let camera, scene, raycaster, renderer; + +let theta = 0; +let INTERSECTED; + +const pointer = new THREE.Vector2(); +const radius = 25; +const frustumSize = 50; + +init(); + +function init() { + const aspect = window.innerWidth / window.innerHeight; + camera = new THREE.OrthographicCamera( + (frustumSize * aspect) / -2, + (frustumSize * aspect) / 2, + frustumSize / 2, + frustumSize / -2, + 0.1, + 100, + ); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(1, 1, 1).normalize(); + scene.add(light); + + const geometry = new THREE.BoxGeometry(); + + for (let i = 0; i < 2000; i++) { + const object = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ color: Math.random() * 0xffffff })); + + object.position.x = Math.random() * 40 - 20; + object.position.y = Math.random() * 40 - 20; + object.position.z = Math.random() * 40 - 20; + + object.rotation.x = Math.random() * 2 * Math.PI; + object.rotation.y = Math.random() * 2 * Math.PI; + object.rotation.z = Math.random() * 2 * Math.PI; + + object.scale.x = Math.random() + 0.5; + object.scale.y = Math.random() + 0.5; + object.scale.z = Math.random() + 0.5; + + scene.add(object); + } + + raycaster = new THREE.Raycaster(); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + document.addEventListener('pointermove', onPointerMove); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const aspect = window.innerWidth / window.innerHeight; + + camera.left = (-frustumSize * aspect) / 2; + camera.right = (frustumSize * aspect) / 2; + camera.top = frustumSize / 2; + camera.bottom = -frustumSize / 2; + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onPointerMove(event) { + pointer.x = (event.clientX / window.innerWidth) * 2 - 1; + pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + theta += 0.1; + + camera.position.x = radius * Math.sin(THREE.MathUtils.degToRad(theta)); + camera.position.y = radius * Math.sin(THREE.MathUtils.degToRad(theta)); + camera.position.z = radius * Math.cos(THREE.MathUtils.degToRad(theta)); + camera.lookAt(scene.position); + + camera.updateMatrixWorld(); + + // find intersections + + raycaster.setFromCamera(pointer, camera); + + const intersects = raycaster.intersectObjects(scene.children, false); + + if (intersects.length > 0) { + if (INTERSECTED != intersects[0].object) { + if (INTERSECTED) INTERSECTED.material.emissive.setHex(INTERSECTED.currentHex); + + INTERSECTED = intersects[0].object; + INTERSECTED.currentHex = INTERSECTED.material.emissive.getHex(); + INTERSECTED.material.emissive.setHex(0xff0000); + } + } else { + if (INTERSECTED) INTERSECTED.material.emissive.setHex(INTERSECTED.currentHex); + + INTERSECTED = null; + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_interactive_lines.ts b/examples-testing/examples/webgl_interactive_lines.ts new file mode 100644 index 000000000..b137c5501 --- /dev/null +++ b/examples-testing/examples/webgl_interactive_lines.ts @@ -0,0 +1,160 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; +let camera, scene, raycaster, renderer, parentTransform, sphereInter; + +const pointer = new THREE.Vector2(); +const radius = 100; +let theta = 0; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + const info = document.createElement('div'); + info.style.position = 'absolute'; + info.style.top = '10px'; + info.style.width = '100%'; + info.style.textAlign = 'center'; + info.innerHTML = + 'three.js webgl - interactive lines'; + container.appendChild(info); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 10000); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + const geometry = new THREE.SphereGeometry(5); + const material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); + + sphereInter = new THREE.Mesh(geometry, material); + sphereInter.visible = false; + scene.add(sphereInter); + + const lineGeometry = new THREE.BufferGeometry(); + const points = []; + + const point = new THREE.Vector3(); + const direction = new THREE.Vector3(); + + for (let i = 0; i < 50; i++) { + direction.x += Math.random() - 0.5; + direction.y += Math.random() - 0.5; + direction.z += Math.random() - 0.5; + direction.normalize().multiplyScalar(10); + + point.add(direction); + points.push(point.x, point.y, point.z); + } + + lineGeometry.setAttribute('position', new THREE.Float32BufferAttribute(points, 3)); + + parentTransform = new THREE.Object3D(); + parentTransform.position.x = Math.random() * 40 - 20; + parentTransform.position.y = Math.random() * 40 - 20; + parentTransform.position.z = Math.random() * 40 - 20; + + parentTransform.rotation.x = Math.random() * 2 * Math.PI; + parentTransform.rotation.y = Math.random() * 2 * Math.PI; + parentTransform.rotation.z = Math.random() * 2 * Math.PI; + + parentTransform.scale.x = Math.random() + 0.5; + parentTransform.scale.y = Math.random() + 0.5; + parentTransform.scale.z = Math.random() + 0.5; + + for (let i = 0; i < 50; i++) { + let object; + + const lineMaterial = new THREE.LineBasicMaterial({ color: Math.random() * 0xffffff }); + + if (Math.random() > 0.5) { + object = new THREE.Line(lineGeometry, lineMaterial); + } else { + object = new THREE.LineSegments(lineGeometry, lineMaterial); + } + + object.position.x = Math.random() * 400 - 200; + object.position.y = Math.random() * 400 - 200; + object.position.z = Math.random() * 400 - 200; + + object.rotation.x = Math.random() * 2 * Math.PI; + object.rotation.y = Math.random() * 2 * Math.PI; + object.rotation.z = Math.random() * 2 * Math.PI; + + object.scale.x = Math.random() + 0.5; + object.scale.y = Math.random() + 0.5; + object.scale.z = Math.random() + 0.5; + + parentTransform.add(object); + } + + scene.add(parentTransform); + + raycaster = new THREE.Raycaster(); + raycaster.params.Line.threshold = 3; + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + document.addEventListener('pointermove', onPointerMove); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onPointerMove(event) { + pointer.x = (event.clientX / window.innerWidth) * 2 - 1; + pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + theta += 0.1; + + camera.position.x = radius * Math.sin(THREE.MathUtils.degToRad(theta)); + camera.position.y = radius * Math.sin(THREE.MathUtils.degToRad(theta)); + camera.position.z = radius * Math.cos(THREE.MathUtils.degToRad(theta)); + camera.lookAt(scene.position); + + camera.updateMatrixWorld(); + + // find intersections + + raycaster.setFromCamera(pointer, camera); + + const intersects = raycaster.intersectObjects(parentTransform.children, true); + + if (intersects.length > 0) { + sphereInter.visible = true; + sphereInter.position.copy(intersects[0].point); + } else { + sphereInter.visible = false; + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_interactive_points.ts b/examples-testing/examples/webgl_interactive_points.ts new file mode 100644 index 000000000..93113b867 --- /dev/null +++ b/examples-testing/examples/webgl_interactive_points.ts @@ -0,0 +1,143 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; + +let renderer, scene, camera, stats; + +let particles; + +const PARTICLE_SIZE = 20; + +let raycaster, intersects; +let pointer, INTERSECTED; + +init(); + +function init() { + const container = document.getElementById('container'); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 250; + + // + + let boxGeometry = new THREE.BoxGeometry(200, 200, 200, 16, 16, 16); + + // if normal and uv attributes are not removed, mergeVertices() can't consolidate indentical vertices with different normal/uv data + + boxGeometry.deleteAttribute('normal'); + boxGeometry.deleteAttribute('uv'); + + boxGeometry = BufferGeometryUtils.mergeVertices(boxGeometry); + + // + + const positionAttribute = boxGeometry.getAttribute('position'); + + const colors = []; + const sizes = []; + + const color = new THREE.Color(); + + for (let i = 0, l = positionAttribute.count; i < l; i++) { + color.setHSL(0.01 + 0.1 * (i / l), 1.0, 0.5); + color.toArray(colors, i * 3); + + sizes[i] = PARTICLE_SIZE * 0.5; + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', positionAttribute); + geometry.setAttribute('customColor', new THREE.Float32BufferAttribute(colors, 3)); + geometry.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1)); + + // + + const material = new THREE.ShaderMaterial({ + uniforms: { + color: { value: new THREE.Color(0xffffff) }, + pointTexture: { value: new THREE.TextureLoader().load('textures/sprites/disc.png') }, + alphaTest: { value: 0.9 }, + }, + vertexShader: document.getElementById('vertexshader').textContent, + fragmentShader: document.getElementById('fragmentshader').textContent, + }); + + // + + particles = new THREE.Points(geometry, material); + scene.add(particles); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + raycaster = new THREE.Raycaster(); + pointer = new THREE.Vector2(); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); + document.addEventListener('pointermove', onPointerMove); +} + +function onPointerMove(event) { + pointer.x = (event.clientX / window.innerWidth) * 2 - 1; + pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + particles.rotation.x += 0.0005; + particles.rotation.y += 0.001; + + const geometry = particles.geometry; + const attributes = geometry.attributes; + + raycaster.setFromCamera(pointer, camera); + + intersects = raycaster.intersectObject(particles); + + if (intersects.length > 0) { + if (INTERSECTED != intersects[0].index) { + attributes.size.array[INTERSECTED] = PARTICLE_SIZE; + + INTERSECTED = intersects[0].index; + + attributes.size.array[INTERSECTED] = PARTICLE_SIZE * 1.25; + attributes.size.needsUpdate = true; + } + } else if (INTERSECTED !== null) { + attributes.size.array[INTERSECTED] = PARTICLE_SIZE; + attributes.size.needsUpdate = true; + INTERSECTED = null; + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_interactive_raycasting_points.ts b/examples-testing/examples/webgl_interactive_raycasting_points.ts new file mode 100644 index 000000000..41c158a43 --- /dev/null +++ b/examples-testing/examples/webgl_interactive_raycasting_points.ts @@ -0,0 +1,220 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let renderer, scene, camera, stats; +let pointclouds; +let raycaster; +let intersection = null; +let spheresIndex = 0; +let clock; +let toggle = 0; + +const pointer = new THREE.Vector2(); +const spheres = []; + +const threshold = 0.1; +const pointSize = 0.05; +const width = 80; +const length = 160; +const rotateY = new THREE.Matrix4().makeRotationY(0.005); + +init(); + +function generatePointCloudGeometry(color, width, length) { + const geometry = new THREE.BufferGeometry(); + const numPoints = width * length; + + const positions = new Float32Array(numPoints * 3); + const colors = new Float32Array(numPoints * 3); + + let k = 0; + + for (let i = 0; i < width; i++) { + for (let j = 0; j < length; j++) { + const u = i / width; + const v = j / length; + const x = u - 0.5; + const y = (Math.cos(u * Math.PI * 4) + Math.sin(v * Math.PI * 8)) / 20; + const z = v - 0.5; + + positions[3 * k] = x; + positions[3 * k + 1] = y; + positions[3 * k + 2] = z; + + const intensity = (y + 0.1) * 5; + colors[3 * k] = color.r * intensity; + colors[3 * k + 1] = color.g * intensity; + colors[3 * k + 2] = color.b * intensity; + + k++; + } + } + + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); + geometry.computeBoundingBox(); + + return geometry; +} + +function generatePointcloud(color, width, length) { + const geometry = generatePointCloudGeometry(color, width, length); + const material = new THREE.PointsMaterial({ size: pointSize, vertexColors: true }); + + return new THREE.Points(geometry, material); +} + +function generateIndexedPointcloud(color, width, length) { + const geometry = generatePointCloudGeometry(color, width, length); + const numPoints = width * length; + const indices = new Uint16Array(numPoints); + + let k = 0; + + for (let i = 0; i < width; i++) { + for (let j = 0; j < length; j++) { + indices[k] = k; + k++; + } + } + + geometry.setIndex(new THREE.BufferAttribute(indices, 1)); + + const material = new THREE.PointsMaterial({ size: pointSize, vertexColors: true }); + + return new THREE.Points(geometry, material); +} + +function generateIndexedWithOffsetPointcloud(color, width, length) { + const geometry = generatePointCloudGeometry(color, width, length); + const numPoints = width * length; + const indices = new Uint16Array(numPoints); + + let k = 0; + + for (let i = 0; i < width; i++) { + for (let j = 0; j < length; j++) { + indices[k] = k; + k++; + } + } + + geometry.setIndex(new THREE.BufferAttribute(indices, 1)); + geometry.addGroup(0, indices.length); + + const material = new THREE.PointsMaterial({ size: pointSize, vertexColors: true }); + + return new THREE.Points(geometry, material); +} + +function init() { + const container = document.getElementById('container'); + + scene = new THREE.Scene(); + + clock = new THREE.Clock(); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.set(10, 10, 10); + camera.lookAt(scene.position); + camera.updateMatrix(); + + // + + const pcBuffer = generatePointcloud(new THREE.Color(1, 0, 0), width, length); + pcBuffer.scale.set(5, 10, 10); + pcBuffer.position.set(-5, 0, 0); + scene.add(pcBuffer); + + const pcIndexed = generateIndexedPointcloud(new THREE.Color(0, 1, 0), width, length); + pcIndexed.scale.set(5, 10, 10); + pcIndexed.position.set(0, 0, 0); + scene.add(pcIndexed); + + const pcIndexedOffset = generateIndexedWithOffsetPointcloud(new THREE.Color(0, 1, 1), width, length); + pcIndexedOffset.scale.set(5, 10, 10); + pcIndexedOffset.position.set(5, 0, 0); + scene.add(pcIndexedOffset); + + pointclouds = [pcBuffer, pcIndexed, pcIndexedOffset]; + + // + + const sphereGeometry = new THREE.SphereGeometry(0.1, 32, 32); + const sphereMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); + + for (let i = 0; i < 40; i++) { + const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); + scene.add(sphere); + spheres.push(sphere); + } + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + raycaster = new THREE.Raycaster(); + raycaster.params.Points.threshold = threshold; + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); + document.addEventListener('pointermove', onPointerMove); +} + +function onPointerMove(event) { + pointer.x = (event.clientX / window.innerWidth) * 2 - 1; + pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + camera.applyMatrix4(rotateY); + camera.updateMatrixWorld(); + + raycaster.setFromCamera(pointer, camera); + + const intersections = raycaster.intersectObjects(pointclouds, false); + intersection = intersections.length > 0 ? intersections[0] : null; + + if (toggle > 0.02 && intersection !== null) { + spheres[spheresIndex].position.copy(intersection.point); + spheres[spheresIndex].scale.set(1, 1, 1); + spheresIndex = (spheresIndex + 1) % spheres.length; + + toggle = 0; + } + + for (let i = 0; i < spheres.length; i++) { + const sphere = spheres[i]; + sphere.scale.multiplyScalar(0.98); + sphere.scale.clampScalar(0.01, 1); + } + + toggle += clock.getDelta(); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_interactive_voxelpainter.ts b/examples-testing/examples/webgl_interactive_voxelpainter.ts new file mode 100644 index 000000000..48b16f3b7 --- /dev/null +++ b/examples-testing/examples/webgl_interactive_voxelpainter.ts @@ -0,0 +1,158 @@ +import * as THREE from 'three'; + +let camera, scene, renderer; +let plane; +let pointer, + raycaster, + isShiftDown = false; + +let rollOverMesh, rollOverMaterial; +let cubeGeo, cubeMaterial; + +const objects = []; + +init(); +render(); + +function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.set(500, 800, 1300); + camera.lookAt(0, 0, 0); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + // roll-over helpers + + const rollOverGeo = new THREE.BoxGeometry(50, 50, 50); + rollOverMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, opacity: 0.5, transparent: true }); + rollOverMesh = new THREE.Mesh(rollOverGeo, rollOverMaterial); + scene.add(rollOverMesh); + + // cubes + + const map = new THREE.TextureLoader().load('textures/square-outline-textured.png'); + map.colorSpace = THREE.SRGBColorSpace; + cubeGeo = new THREE.BoxGeometry(50, 50, 50); + cubeMaterial = new THREE.MeshLambertMaterial({ color: 0xfeb74c, map: map }); + + // grid + + const gridHelper = new THREE.GridHelper(1000, 20); + scene.add(gridHelper); + + // + + raycaster = new THREE.Raycaster(); + pointer = new THREE.Vector2(); + + const geometry = new THREE.PlaneGeometry(1000, 1000); + geometry.rotateX(-Math.PI / 2); + + plane = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ visible: false })); + scene.add(plane); + + objects.push(plane); + + // lights + + const ambientLight = new THREE.AmbientLight(0x606060, 3); + scene.add(ambientLight); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 3); + directionalLight.position.set(1, 0.75, 0.5).normalize(); + scene.add(directionalLight); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + document.addEventListener('pointermove', onPointerMove); + document.addEventListener('pointerdown', onPointerDown); + document.addEventListener('keydown', onDocumentKeyDown); + document.addEventListener('keyup', onDocumentKeyUp); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function onPointerMove(event) { + pointer.set((event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1); + + raycaster.setFromCamera(pointer, camera); + + const intersects = raycaster.intersectObjects(objects, false); + + if (intersects.length > 0) { + const intersect = intersects[0]; + + rollOverMesh.position.copy(intersect.point).add(intersect.face.normal); + rollOverMesh.position.divideScalar(50).floor().multiplyScalar(50).addScalar(25); + + render(); + } +} + +function onPointerDown(event) { + pointer.set((event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1); + + raycaster.setFromCamera(pointer, camera); + + const intersects = raycaster.intersectObjects(objects, false); + + if (intersects.length > 0) { + const intersect = intersects[0]; + + // delete cube + + if (isShiftDown) { + if (intersect.object !== plane) { + scene.remove(intersect.object); + + objects.splice(objects.indexOf(intersect.object), 1); + } + + // create cube + } else { + const voxel = new THREE.Mesh(cubeGeo, cubeMaterial); + voxel.position.copy(intersect.point).add(intersect.face.normal); + voxel.position.divideScalar(50).floor().multiplyScalar(50).addScalar(25); + scene.add(voxel); + + objects.push(voxel); + } + + render(); + } +} + +function onDocumentKeyDown(event) { + switch (event.keyCode) { + case 16: + isShiftDown = true; + break; + } +} + +function onDocumentKeyUp(event) { + switch (event.keyCode) { + case 16: + isShiftDown = false; + break; + } +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_layers.ts b/examples-testing/examples/webgl_layers.ts new file mode 100644 index 000000000..8bdcda7f9 --- /dev/null +++ b/examples-testing/examples/webgl_layers.ts @@ -0,0 +1,125 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let container, stats; +let camera, scene, renderer; + +let theta = 0; +const radius = 5; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100); + camera.layers.enable(0); // enabled by default + camera.layers.enable(1); + camera.layers.enable(2); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + const light = new THREE.PointLight(0xffffff, 3, 0, 0); + light.layers.enable(0); + light.layers.enable(1); + light.layers.enable(2); + + scene.add(camera); + camera.add(light); + + const colors = [0xff0000, 0x00ff00, 0x0000ff]; + const geometry = new THREE.BoxGeometry(); + + for (let i = 0; i < 300; i++) { + const layer = i % 3; + + const object = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ color: colors[layer] })); + + object.position.x = Math.random() * 40 - 20; + object.position.y = Math.random() * 40 - 20; + object.position.z = Math.random() * 40 - 20; + + object.rotation.x = Math.random() * 2 * Math.PI; + object.rotation.y = Math.random() * 2 * Math.PI; + object.rotation.z = Math.random() * 2 * Math.PI; + + object.scale.x = Math.random() + 0.5; + object.scale.y = Math.random() + 0.5; + object.scale.z = Math.random() + 0.5; + + object.layers.set(layer); + + scene.add(object); + } + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + const layers = { + 'toggle red': function () { + camera.layers.toggle(0); + }, + + 'toggle green': function () { + camera.layers.toggle(1); + }, + + 'toggle blue': function () { + camera.layers.toggle(2); + }, + + 'enable all': function () { + camera.layers.enableAll(); + }, + + 'disable all': function () { + camera.layers.disableAll(); + }, + }; + + // + // Init gui + const gui = new GUI(); + gui.add(layers, 'toggle red'); + gui.add(layers, 'toggle green'); + gui.add(layers, 'toggle blue'); + gui.add(layers, 'enable all'); + gui.add(layers, 'disable all'); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + theta += 0.1; + + camera.position.x = radius * Math.sin(THREE.MathUtils.degToRad(theta)); + camera.position.y = radius * Math.sin(THREE.MathUtils.degToRad(theta)); + camera.position.z = radius * Math.cos(THREE.MathUtils.degToRad(theta)); + camera.lookAt(scene.position); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_lensflares.ts b/examples-testing/examples/webgl_lensflares.ts new file mode 100644 index 000000000..230cebfa0 --- /dev/null +++ b/examples-testing/examples/webgl_lensflares.ts @@ -0,0 +1,137 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { FlyControls } from 'three/addons/controls/FlyControls.js'; +import { Lensflare, LensflareElement } from 'three/addons/objects/Lensflare.js'; + +let container, stats; + +let camera, scene, renderer; +let controls; + +const clock = new THREE.Clock(); + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + // camera + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 15000); + camera.position.z = 250; + + // scene + + scene = new THREE.Scene(); + scene.background = new THREE.Color().setHSL(0.51, 0.4, 0.01, THREE.SRGBColorSpace); + scene.fog = new THREE.Fog(scene.background, 3500, 15000); + + // world + + const s = 250; + + const geometry = new THREE.BoxGeometry(s, s, s); + const material = new THREE.MeshPhongMaterial({ color: 0xffffff, specular: 0xffffff, shininess: 50 }); + + for (let i = 0; i < 3000; i++) { + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.x = 8000 * (2.0 * Math.random() - 1.0); + mesh.position.y = 8000 * (2.0 * Math.random() - 1.0); + mesh.position.z = 8000 * (2.0 * Math.random() - 1.0); + + mesh.rotation.x = Math.random() * Math.PI; + mesh.rotation.y = Math.random() * Math.PI; + mesh.rotation.z = Math.random() * Math.PI; + + mesh.matrixAutoUpdate = false; + mesh.updateMatrix(); + + scene.add(mesh); + } + + // lights + + const dirLight = new THREE.DirectionalLight(0xffffff, 0.15); + dirLight.position.set(0, -1, 0).normalize(); + dirLight.color.setHSL(0.1, 0.7, 0.5); + scene.add(dirLight); + + // lensflares + const textureLoader = new THREE.TextureLoader(); + + const textureFlare0 = textureLoader.load('textures/lensflare/lensflare0.png'); + const textureFlare3 = textureLoader.load('textures/lensflare/lensflare3.png'); + + addLight(0.55, 0.9, 0.5, 5000, 0, -1000); + addLight(0.08, 0.8, 0.5, 0, 0, -1000); + addLight(0.995, 0.5, 0.9, 5000, 5000, -1000); + + function addLight(h, s, l, x, y, z) { + const light = new THREE.PointLight(0xffffff, 1.5, 2000, 0); + light.color.setHSL(h, s, l); + light.position.set(x, y, z); + scene.add(light); + + const lensflare = new Lensflare(); + lensflare.addElement(new LensflareElement(textureFlare0, 700, 0, light.color)); + lensflare.addElement(new LensflareElement(textureFlare3, 60, 0.6)); + lensflare.addElement(new LensflareElement(textureFlare3, 70, 0.7)); + lensflare.addElement(new LensflareElement(textureFlare3, 120, 0.9)); + lensflare.addElement(new LensflareElement(textureFlare3, 70, 1)); + light.add(lensflare); + } + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + controls = new FlyControls(camera, renderer.domElement); + + controls.movementSpeed = 2500; + controls.domElement = container; + controls.rollSpeed = Math.PI / 6; + controls.autoForward = false; + controls.dragToLook = false; + + // stats + + stats = new Stats(); + container.appendChild(stats.dom); + + // events + + window.addEventListener('resize', onWindowResize); +} + +// + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + const delta = clock.getDelta(); + + controls.update(delta); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_lightprobe.ts b/examples-testing/examples/webgl_lightprobe.ts new file mode 100644 index 000000000..2efcad52a --- /dev/null +++ b/examples-testing/examples/webgl_lightprobe.ts @@ -0,0 +1,133 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { LightProbeGenerator } from 'three/addons/lights/LightProbeGenerator.js'; + +let mesh, renderer, scene, camera; + +let gui; + +let lightProbe; +let directionalLight; + +// linear color space +const API = { + lightProbeIntensity: 1.0, + directionalLightIntensity: 0.6, + envMapIntensity: 1, +}; + +init(); + +function init() { + // renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + // tone mapping + renderer.toneMapping = THREE.NoToneMapping; + + // scene + scene = new THREE.Scene(); + + // camera + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 0, 30); + + // controls + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); + controls.minDistance = 10; + controls.maxDistance = 50; + controls.enablePan = false; + + // probe + lightProbe = new THREE.LightProbe(); + scene.add(lightProbe); + + // light + directionalLight = new THREE.DirectionalLight(0xffffff, API.directionalLightIntensity); + directionalLight.position.set(10, 10, 10); + scene.add(directionalLight); + + // envmap + const genCubeUrls = function (prefix, postfix) { + return [ + prefix + 'px' + postfix, + prefix + 'nx' + postfix, + prefix + 'py' + postfix, + prefix + 'ny' + postfix, + prefix + 'pz' + postfix, + prefix + 'nz' + postfix, + ]; + }; + + const urls = genCubeUrls('textures/cube/pisa/', '.png'); + + new THREE.CubeTextureLoader().load(urls, function (cubeTexture) { + scene.background = cubeTexture; + + lightProbe.copy(LightProbeGenerator.fromCubeTexture(cubeTexture)); + + const geometry = new THREE.SphereGeometry(5, 64, 32); + //const geometry = new THREE.TorusKnotGeometry( 4, 1.5, 256, 32, 2, 3 ); + + const material = new THREE.MeshStandardMaterial({ + color: 0xffffff, + metalness: 0, + roughness: 0, + envMap: cubeTexture, + envMapIntensity: API.envMapIntensity, + }); + + // mesh + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + render(); + }); + + // gui + gui = new GUI({ title: 'Intensity' }); + + gui.add(API, 'lightProbeIntensity', 0, 1, 0.02) + .name('light probe') + .onChange(function () { + lightProbe.intensity = API.lightProbeIntensity; + render(); + }); + + gui.add(API, 'directionalLightIntensity', 0, 1, 0.02) + .name('directional light') + .onChange(function () { + directionalLight.intensity = API.directionalLightIntensity; + render(); + }); + + gui.add(API, 'envMapIntensity', 0, 1, 0.02) + .name('envMap') + .onChange(function () { + mesh.material.envMapIntensity = API.envMapIntensity; + render(); + }); + + // listener + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_lightprobe_cubecamera.ts b/examples-testing/examples/webgl_lightprobe_cubecamera.ts new file mode 100644 index 000000000..c714d2978 --- /dev/null +++ b/examples-testing/examples/webgl_lightprobe_cubecamera.ts @@ -0,0 +1,83 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { LightProbeHelper } from 'three/addons/helpers/LightProbeHelper.js'; +import { LightProbeGenerator } from 'three/addons/lights/LightProbeGenerator.js'; + +let renderer, scene, camera, cubeCamera; + +let lightProbe; + +init(); + +function init() { + // renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + // scene + scene = new THREE.Scene(); + + // camera + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 0, 30); + + const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256); + + cubeCamera = new THREE.CubeCamera(1, 1000, cubeRenderTarget); + + // controls + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); + controls.minDistance = 10; + controls.maxDistance = 50; + controls.enablePan = false; + + // probe + lightProbe = new THREE.LightProbe(); + scene.add(lightProbe); + + // envmap + const genCubeUrls = function (prefix, postfix) { + return [ + prefix + 'px' + postfix, + prefix + 'nx' + postfix, + prefix + 'py' + postfix, + prefix + 'ny' + postfix, + prefix + 'pz' + postfix, + prefix + 'nz' + postfix, + ]; + }; + + const urls = genCubeUrls('textures/cube/pisa/', '.png'); + + new THREE.CubeTextureLoader().load(urls, function (cubeTexture) { + scene.background = cubeTexture; + + cubeCamera.update(renderer, scene); + + lightProbe.copy(LightProbeGenerator.fromCubeRenderTarget(renderer, cubeRenderTarget)); + + scene.add(new LightProbeHelper(lightProbe, 5)); + + render(); + }); + + // listener + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_lights_hemisphere.ts b/examples-testing/examples/webgl_lights_hemisphere.ts new file mode 100644 index 000000000..15bc76099 --- /dev/null +++ b/examples-testing/examples/webgl_lights_hemisphere.ts @@ -0,0 +1,188 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +let camera, scene, renderer; +const mixers = []; +let stats; + +const clock = new THREE.Clock(); + +init(); + +function init() { + const container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 1, 5000); + camera.position.set(0, 0, 250); + + scene = new THREE.Scene(); + scene.background = new THREE.Color().setHSL(0.6, 0, 1); + scene.fog = new THREE.Fog(scene.background, 1, 5000); + + // LIGHTS + + const hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 2); + hemiLight.color.setHSL(0.6, 1, 0.6); + hemiLight.groundColor.setHSL(0.095, 1, 0.75); + hemiLight.position.set(0, 50, 0); + scene.add(hemiLight); + + const hemiLightHelper = new THREE.HemisphereLightHelper(hemiLight, 10); + scene.add(hemiLightHelper); + + // + + const dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.color.setHSL(0.1, 1, 0.95); + dirLight.position.set(-1, 1.75, 1); + dirLight.position.multiplyScalar(30); + scene.add(dirLight); + + dirLight.castShadow = true; + + dirLight.shadow.mapSize.width = 2048; + dirLight.shadow.mapSize.height = 2048; + + const d = 50; + + dirLight.shadow.camera.left = -d; + dirLight.shadow.camera.right = d; + dirLight.shadow.camera.top = d; + dirLight.shadow.camera.bottom = -d; + + dirLight.shadow.camera.far = 3500; + dirLight.shadow.bias = -0.0001; + + const dirLightHelper = new THREE.DirectionalLightHelper(dirLight, 10); + scene.add(dirLightHelper); + + // GROUND + + const groundGeo = new THREE.PlaneGeometry(10000, 10000); + const groundMat = new THREE.MeshLambertMaterial({ color: 0xffffff }); + groundMat.color.setHSL(0.095, 1, 0.75); + + const ground = new THREE.Mesh(groundGeo, groundMat); + ground.position.y = -33; + ground.rotation.x = -Math.PI / 2; + ground.receiveShadow = true; + scene.add(ground); + + // SKYDOME + + const vertexShader = document.getElementById('vertexShader').textContent; + const fragmentShader = document.getElementById('fragmentShader').textContent; + const uniforms = { + topColor: { value: new THREE.Color(0x0077ff) }, + bottomColor: { value: new THREE.Color(0xffffff) }, + offset: { value: 33 }, + exponent: { value: 0.6 }, + }; + uniforms['topColor'].value.copy(hemiLight.color); + + scene.fog.color.copy(uniforms['bottomColor'].value); + + const skyGeo = new THREE.SphereGeometry(4000, 32, 15); + const skyMat = new THREE.ShaderMaterial({ + uniforms: uniforms, + vertexShader: vertexShader, + fragmentShader: fragmentShader, + side: THREE.BackSide, + }); + + const sky = new THREE.Mesh(skyGeo, skyMat); + scene.add(sky); + + // MODEL + + const loader = new GLTFLoader(); + + loader.load('models/gltf/Flamingo.glb', function (gltf) { + const mesh = gltf.scene.children[0]; + + const s = 0.35; + mesh.scale.set(s, s, s); + mesh.position.y = 15; + mesh.rotation.y = -1; + + mesh.castShadow = true; + mesh.receiveShadow = true; + + scene.add(mesh); + + const mixer = new THREE.AnimationMixer(mesh); + mixer.clipAction(gltf.animations[0]).setDuration(1).play(); + mixers.push(mixer); + }); + + // RENDERER + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + renderer.shadowMap.enabled = true; + + // STATS + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + const params = { + toggleHemisphereLight: function () { + hemiLight.visible = !hemiLight.visible; + hemiLightHelper.visible = !hemiLightHelper.visible; + }, + toggleDirectionalLight: function () { + dirLight.visible = !dirLight.visible; + dirLightHelper.visible = !dirLightHelper.visible; + }, + shadowIntensity: 1, + }; + + const gui = new GUI(); + + gui.add(params, 'toggleHemisphereLight').name('toggle hemisphere light'); + gui.add(params, 'toggleDirectionalLight').name('toggle directional light'); + gui.add(params, 'shadowIntensity', 0, 1) + .name('shadow intensity') + .onChange(value => { + dirLight.shadow.intensity = value; + }); + gui.open(); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + const delta = clock.getDelta(); + + for (let i = 0; i < mixers.length; i++) { + mixers[i].update(delta); + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_lights_physical.ts b/examples-testing/examples/webgl_lights_physical.ts new file mode 100644 index 000000000..707ef200e --- /dev/null +++ b/examples-testing/examples/webgl_lights_physical.ts @@ -0,0 +1,237 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer, bulbLight, bulbMat, hemiLight, stats; +let ballMat, cubeMat, floorMat; + +let previousShadowMap = false; + +// ref for lumens: http://www.power-sure.com/lumens.htm +const bulbLuminousPowers = { + '110000 lm (1000W)': 110000, + '3500 lm (300W)': 3500, + '1700 lm (100W)': 1700, + '800 lm (60W)': 800, + '400 lm (40W)': 400, + '180 lm (25W)': 180, + '20 lm (4W)': 20, + Off: 0, +}; + +// ref for solar irradiances: https://en.wikipedia.org/wiki/Lux +const hemiLuminousIrradiances = { + '0.0001 lx (Moonless Night)': 0.0001, + '0.002 lx (Night Airglow)': 0.002, + '0.5 lx (Full Moon)': 0.5, + '3.4 lx (City Twilight)': 3.4, + '50 lx (Living Room)': 50, + '100 lx (Very Overcast)': 100, + '350 lx (Office Room)': 350, + '400 lx (Sunrise/Sunset)': 400, + '1000 lx (Overcast)': 1000, + '18000 lx (Daylight)': 18000, + '50000 lx (Direct Sun)': 50000, +}; + +const params = { + shadows: true, + exposure: 0.68, + bulbPower: Object.keys(bulbLuminousPowers)[4], + hemiIrradiance: Object.keys(hemiLuminousIrradiances)[0], +}; + +init(); + +function init() { + const container = document.getElementById('container'); + + stats = new Stats(); + container.appendChild(stats.dom); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.x = -4; + camera.position.z = 4; + camera.position.y = 2; + + scene = new THREE.Scene(); + + const bulbGeometry = new THREE.SphereGeometry(0.02, 16, 8); + bulbLight = new THREE.PointLight(0xffee88, 1, 100, 2); + + bulbMat = new THREE.MeshStandardMaterial({ + emissive: 0xffffee, + emissiveIntensity: 1, + color: 0x000000, + }); + bulbLight.add(new THREE.Mesh(bulbGeometry, bulbMat)); + bulbLight.position.set(0, 2, 0); + bulbLight.castShadow = true; + scene.add(bulbLight); + + hemiLight = new THREE.HemisphereLight(0xddeeff, 0x0f0e0d, 0.02); + scene.add(hemiLight); + + floorMat = new THREE.MeshStandardMaterial({ + roughness: 0.8, + color: 0xffffff, + metalness: 0.2, + bumpScale: 1, + }); + const textureLoader = new THREE.TextureLoader(); + textureLoader.load('textures/hardwood2_diffuse.jpg', function (map) { + map.wrapS = THREE.RepeatWrapping; + map.wrapT = THREE.RepeatWrapping; + map.anisotropy = 4; + map.repeat.set(10, 24); + map.colorSpace = THREE.SRGBColorSpace; + floorMat.map = map; + floorMat.needsUpdate = true; + }); + textureLoader.load('textures/hardwood2_bump.jpg', function (map) { + map.wrapS = THREE.RepeatWrapping; + map.wrapT = THREE.RepeatWrapping; + map.anisotropy = 4; + map.repeat.set(10, 24); + floorMat.bumpMap = map; + floorMat.needsUpdate = true; + }); + textureLoader.load('textures/hardwood2_roughness.jpg', function (map) { + map.wrapS = THREE.RepeatWrapping; + map.wrapT = THREE.RepeatWrapping; + map.anisotropy = 4; + map.repeat.set(10, 24); + floorMat.roughnessMap = map; + floorMat.needsUpdate = true; + }); + + cubeMat = new THREE.MeshStandardMaterial({ + roughness: 0.7, + color: 0xffffff, + bumpScale: 1, + metalness: 0.2, + }); + textureLoader.load('textures/brick_diffuse.jpg', function (map) { + map.wrapS = THREE.RepeatWrapping; + map.wrapT = THREE.RepeatWrapping; + map.anisotropy = 4; + map.repeat.set(1, 1); + map.colorSpace = THREE.SRGBColorSpace; + cubeMat.map = map; + cubeMat.needsUpdate = true; + }); + textureLoader.load('textures/brick_bump.jpg', function (map) { + map.wrapS = THREE.RepeatWrapping; + map.wrapT = THREE.RepeatWrapping; + map.anisotropy = 4; + map.repeat.set(1, 1); + cubeMat.bumpMap = map; + cubeMat.needsUpdate = true; + }); + + ballMat = new THREE.MeshStandardMaterial({ + color: 0xffffff, + roughness: 0.5, + metalness: 1.0, + }); + textureLoader.load('textures/planets/earth_atmos_2048.jpg', function (map) { + map.anisotropy = 4; + map.colorSpace = THREE.SRGBColorSpace; + ballMat.map = map; + ballMat.needsUpdate = true; + }); + textureLoader.load('textures/planets/earth_specular_2048.jpg', function (map) { + map.anisotropy = 4; + map.colorSpace = THREE.SRGBColorSpace; + ballMat.metalnessMap = map; + ballMat.needsUpdate = true; + }); + + const floorGeometry = new THREE.PlaneGeometry(20, 20); + const floorMesh = new THREE.Mesh(floorGeometry, floorMat); + floorMesh.receiveShadow = true; + floorMesh.rotation.x = -Math.PI / 2.0; + scene.add(floorMesh); + + const ballGeometry = new THREE.SphereGeometry(0.25, 32, 32); + const ballMesh = new THREE.Mesh(ballGeometry, ballMat); + ballMesh.position.set(1, 0.25, 1); + ballMesh.rotation.y = Math.PI; + ballMesh.castShadow = true; + scene.add(ballMesh); + + const boxGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5); + const boxMesh = new THREE.Mesh(boxGeometry, cubeMat); + boxMesh.position.set(-0.5, 0.25, -1); + boxMesh.castShadow = true; + scene.add(boxMesh); + + const boxMesh2 = new THREE.Mesh(boxGeometry, cubeMat); + boxMesh2.position.set(0, 0.25, -5); + boxMesh2.castShadow = true; + scene.add(boxMesh2); + + const boxMesh3 = new THREE.Mesh(boxGeometry, cubeMat); + boxMesh3.position.set(7, 0.25, 0); + boxMesh3.castShadow = true; + scene.add(boxMesh3); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + renderer.toneMapping = THREE.ReinhardToneMapping; + container.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 1; + controls.maxDistance = 20; + + window.addEventListener('resize', onWindowResize); + + const gui = new GUI(); + + gui.add(params, 'hemiIrradiance', Object.keys(hemiLuminousIrradiances)); + gui.add(params, 'bulbPower', Object.keys(bulbLuminousPowers)); + gui.add(params, 'exposure', 0, 1); + gui.add(params, 'shadows'); + gui.open(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + renderer.toneMappingExposure = Math.pow(params.exposure, 5.0); // to allow for very bright scenes. + renderer.shadowMap.enabled = params.shadows; + bulbLight.castShadow = params.shadows; + + if (params.shadows !== previousShadowMap) { + ballMat.needsUpdate = true; + cubeMat.needsUpdate = true; + floorMat.needsUpdate = true; + previousShadowMap = params.shadows; + } + + bulbLight.power = bulbLuminousPowers[params.bulbPower]; + bulbMat.emissiveIntensity = bulbLight.intensity / Math.pow(0.02, 2.0); // convert from intensity to irradiance at bulb surface + + hemiLight.intensity = hemiLuminousIrradiances[params.hemiIrradiance]; + const time = Date.now() * 0.0005; + + bulbLight.position.y = Math.cos(time) * 0.75 + 1.25; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_lights_pointlights.ts b/examples-testing/examples/webgl_lights_pointlights.ts new file mode 100644 index 000000000..ea95070ce --- /dev/null +++ b/examples-testing/examples/webgl_lights_pointlights.ts @@ -0,0 +1,100 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'; + +let camera, scene, renderer, light1, light2, light3, light4, object, stats; + +const clock = new THREE.Clock(); + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 100; + + scene = new THREE.Scene(); + + //model + + const loader = new OBJLoader(); + loader.load('models/obj/walt/WaltHead.obj', function (obj) { + object = obj; + object.scale.multiplyScalar(0.8); + object.position.y = -30; + scene.add(object); + }); + + const sphere = new THREE.SphereGeometry(0.5, 16, 8); + + //lights + + light1 = new THREE.PointLight(0xff0040, 400); + light1.add(new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({ color: 0xff0040 }))); + scene.add(light1); + + light2 = new THREE.PointLight(0x0040ff, 400); + light2.add(new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({ color: 0x0040ff }))); + scene.add(light2); + + light3 = new THREE.PointLight(0x80ff80, 400); + light3.add(new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({ color: 0x80ff80 }))); + scene.add(light3); + + light4 = new THREE.PointLight(0xffaa00, 400); + light4.add(new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({ color: 0xffaa00 }))); + scene.add(light4); + + //renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + //stats + + stats = new Stats(); + document.body.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + const time = Date.now() * 0.0005; + const delta = clock.getDelta(); + + if (object) object.rotation.y -= 0.5 * delta; + + light1.position.x = Math.sin(time * 0.7) * 30; + light1.position.y = Math.cos(time * 0.5) * 40; + light1.position.z = Math.cos(time * 0.3) * 30; + + light2.position.x = Math.cos(time * 0.3) * 30; + light2.position.y = Math.sin(time * 0.5) * 40; + light2.position.z = Math.sin(time * 0.7) * 30; + + light3.position.x = Math.sin(time * 0.7) * 30; + light3.position.y = Math.cos(time * 0.3) * 40; + light3.position.z = Math.sin(time * 0.5) * 30; + + light4.position.x = Math.sin(time * 0.3) * 30; + light4.position.y = Math.cos(time * 0.7) * 40; + light4.position.z = Math.sin(time * 0.5) * 30; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_lights_rectarealight.ts b/examples-testing/examples/webgl_lights_rectarealight.ts new file mode 100644 index 000000000..b841fa6b5 --- /dev/null +++ b/examples-testing/examples/webgl_lights_rectarealight.ts @@ -0,0 +1,79 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { RectAreaLightHelper } from 'three/addons/helpers/RectAreaLightHelper.js'; +import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js'; + +let renderer, scene, camera; +let stats, meshKnot; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animation); + document.body.appendChild(renderer.domElement); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 5, -15); + + scene = new THREE.Scene(); + + RectAreaLightUniformsLib.init(); + + const rectLight1 = new THREE.RectAreaLight(0xff0000, 5, 4, 10); + rectLight1.position.set(-5, 5, 5); + scene.add(rectLight1); + + const rectLight2 = new THREE.RectAreaLight(0x00ff00, 5, 4, 10); + rectLight2.position.set(0, 5, 5); + scene.add(rectLight2); + + const rectLight3 = new THREE.RectAreaLight(0x0000ff, 5, 4, 10); + rectLight3.position.set(5, 5, 5); + scene.add(rectLight3); + + scene.add(new RectAreaLightHelper(rectLight1)); + scene.add(new RectAreaLightHelper(rectLight2)); + scene.add(new RectAreaLightHelper(rectLight3)); + + const geoFloor = new THREE.BoxGeometry(2000, 0.1, 2000); + const matStdFloor = new THREE.MeshStandardMaterial({ color: 0xbcbcbc, roughness: 0.1, metalness: 0 }); + const mshStdFloor = new THREE.Mesh(geoFloor, matStdFloor); + scene.add(mshStdFloor); + + const geoKnot = new THREE.TorusKnotGeometry(1.5, 0.5, 200, 16); + const matKnot = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0, metalness: 0 }); + meshKnot = new THREE.Mesh(geoKnot, matKnot); + meshKnot.position.set(0, 5, 0); + scene.add(meshKnot); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.copy(meshKnot.position); + controls.update(); + + // + + window.addEventListener('resize', onWindowResize); + + stats = new Stats(); + document.body.appendChild(stats.dom); +} + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); +} + +function animation(time) { + meshKnot.rotation.y = time / 1000; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_lights_spotlight.ts b/examples-testing/examples/webgl_lights_spotlight.ts new file mode 100644 index 000000000..894abaf6f --- /dev/null +++ b/examples-testing/examples/webgl_lights_spotlight.ts @@ -0,0 +1,183 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { PLYLoader } from 'three/addons/loaders/PLYLoader.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let renderer, scene, camera; + +let spotLight, lightHelper; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.PCFSoftShadowMap; + + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1; + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(7, 4, 1); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 2; + controls.maxDistance = 10; + controls.maxPolarAngle = Math.PI / 2; + controls.target.set(0, 1, 0); + controls.update(); + + const ambient = new THREE.HemisphereLight(0xffffff, 0x8d8d8d, 0.15); + scene.add(ambient); + + const loader = new THREE.TextureLoader().setPath('textures/'); + const filenames = ['disturb.jpg', 'colors.png', 'uv_grid_opengl.jpg']; + + const textures = { none: null }; + + for (let i = 0; i < filenames.length; i++) { + const filename = filenames[i]; + + const texture = loader.load(filename); + texture.minFilter = THREE.LinearFilter; + texture.magFilter = THREE.LinearFilter; + texture.colorSpace = THREE.SRGBColorSpace; + + textures[filename] = texture; + } + + spotLight = new THREE.SpotLight(0xffffff, 100); + spotLight.position.set(2.5, 5, 2.5); + spotLight.angle = Math.PI / 6; + spotLight.penumbra = 1; + spotLight.decay = 2; + spotLight.distance = 0; + spotLight.map = textures['disturb.jpg']; + + spotLight.castShadow = true; + spotLight.shadow.mapSize.width = 1024; + spotLight.shadow.mapSize.height = 1024; + spotLight.shadow.camera.near = 1; + spotLight.shadow.camera.far = 10; + spotLight.shadow.focus = 1; + scene.add(spotLight); + + lightHelper = new THREE.SpotLightHelper(spotLight); + scene.add(lightHelper); + + // + + const geometry = new THREE.PlaneGeometry(200, 200); + const material = new THREE.MeshLambertMaterial({ color: 0xbcbcbc }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.set(0, -1, 0); + mesh.rotation.x = -Math.PI / 2; + mesh.receiveShadow = true; + scene.add(mesh); + + // + + new PLYLoader().load('models/ply/binary/Lucy100k.ply', function (geometry) { + geometry.scale(0.0024, 0.0024, 0.0024); + geometry.computeVertexNormals(); + + const material = new THREE.MeshLambertMaterial(); + + const mesh = new THREE.Mesh(geometry, material); + mesh.rotation.y = -Math.PI / 2; + mesh.position.y = 0.8; + mesh.castShadow = true; + mesh.receiveShadow = true; + scene.add(mesh); + }); + + window.addEventListener('resize', onWindowResize); + + // GUI + + const gui = new GUI(); + + const params = { + map: textures['disturb.jpg'], + color: spotLight.color.getHex(), + intensity: spotLight.intensity, + distance: spotLight.distance, + angle: spotLight.angle, + penumbra: spotLight.penumbra, + decay: spotLight.decay, + focus: spotLight.shadow.focus, + shadows: true, + }; + + gui.add(params, 'map', textures).onChange(function (val) { + spotLight.map = val; + }); + + gui.addColor(params, 'color').onChange(function (val) { + spotLight.color.setHex(val); + }); + + gui.add(params, 'intensity', 0, 500).onChange(function (val) { + spotLight.intensity = val; + }); + + gui.add(params, 'distance', 0, 20).onChange(function (val) { + spotLight.distance = val; + }); + + gui.add(params, 'angle', 0, Math.PI / 3).onChange(function (val) { + spotLight.angle = val; + }); + + gui.add(params, 'penumbra', 0, 1).onChange(function (val) { + spotLight.penumbra = val; + }); + + gui.add(params, 'decay', 1, 2).onChange(function (val) { + spotLight.decay = val; + }); + + gui.add(params, 'focus', 0, 1).onChange(function (val) { + spotLight.shadow.focus = val; + }); + + gui.add(params, 'shadows').onChange(function (val) { + renderer.shadowMap.enabled = val; + + scene.traverse(function (child) { + if (child.material) { + child.material.needsUpdate = true; + } + }); + }); + + gui.open(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const time = performance.now() / 3000; + + spotLight.position.x = Math.cos(time) * 2.5; + spotLight.position.z = Math.sin(time) * 2.5; + + lightHelper.update(); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_lights_spotlights.ts b/examples-testing/examples/webgl_lights_spotlights.ts new file mode 100644 index 000000000..70caf5a58 --- /dev/null +++ b/examples-testing/examples/webgl_lights_spotlights.ts @@ -0,0 +1,133 @@ +import * as THREE from 'three'; +import TWEEN from 'three/addons/libs/tween.module.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +const renderer = new THREE.WebGLRenderer({ antialias: true }); +renderer.setPixelRatio(window.devicePixelRatio); +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.setAnimationLoop(animate); + +const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 100); + +const controls = new OrbitControls(camera, renderer.domElement); + +const scene = new THREE.Scene(); + +const matFloor = new THREE.MeshPhongMaterial({ color: 0x808080 }); +const matBox = new THREE.MeshPhongMaterial({ color: 0xaaaaaa }); + +const geoFloor = new THREE.PlaneGeometry(100, 100); +const geoBox = new THREE.BoxGeometry(0.3, 0.1, 0.2); + +const mshFloor = new THREE.Mesh(geoFloor, matFloor); +mshFloor.rotation.x = -Math.PI * 0.5; +const mshBox = new THREE.Mesh(geoBox, matBox); + +const ambient = new THREE.AmbientLight(0x444444); + +const spotLight1 = createSpotlight(0xff7f00); +const spotLight2 = createSpotlight(0x00ff7f); +const spotLight3 = createSpotlight(0x7f00ff); + +let lightHelper1, lightHelper2, lightHelper3; + +function init() { + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.PCFSoftShadowMap; + + camera.position.set(4.6, 2.2, -2.1); + + spotLight1.position.set(1.5, 4, 4.5); + spotLight2.position.set(0, 4, 3.5); + spotLight3.position.set(-1.5, 4, 4.5); + + lightHelper1 = new THREE.SpotLightHelper(spotLight1); + lightHelper2 = new THREE.SpotLightHelper(spotLight2); + lightHelper3 = new THREE.SpotLightHelper(spotLight3); + + mshFloor.receiveShadow = true; + mshFloor.position.set(0, -0.05, 0); + + mshBox.castShadow = true; + mshBox.receiveShadow = true; + mshBox.position.set(0, 0.5, 0); + + scene.add(mshFloor); + scene.add(mshBox); + scene.add(ambient); + scene.add(spotLight1, spotLight2, spotLight3); + scene.add(lightHelper1, lightHelper2, lightHelper3); + + document.body.appendChild(renderer.domElement); + window.addEventListener('resize', onWindowResize); + + controls.target.set(0, 0.5, 0); + controls.maxPolarAngle = Math.PI / 2; + controls.minDistance = 1; + controls.maxDistance = 10; + controls.update(); +} + +function createSpotlight(color) { + const newObj = new THREE.SpotLight(color, 10); + + newObj.castShadow = true; + newObj.angle = 0.3; + newObj.penumbra = 0.2; + newObj.decay = 2; + newObj.distance = 50; + + return newObj; +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function tween(light) { + new TWEEN.Tween(light) + .to( + { + angle: Math.random() * 0.7 + 0.1, + penumbra: Math.random() + 1, + }, + Math.random() * 3000 + 2000, + ) + .easing(TWEEN.Easing.Quadratic.Out) + .start(); + + new TWEEN.Tween(light.position) + .to( + { + x: Math.random() * 3 - 1.5, + y: Math.random() * 1 + 1.5, + z: Math.random() * 3 - 1.5, + }, + Math.random() * 3000 + 2000, + ) + .easing(TWEEN.Easing.Quadratic.Out) + .start(); +} + +function updateTweens() { + tween(spotLight1); + tween(spotLight2); + tween(spotLight3); + + setTimeout(updateTweens, 5000); +} + +function animate() { + TWEEN.update(); + + if (lightHelper1) lightHelper1.update(); + if (lightHelper2) lightHelper2.update(); + if (lightHelper3) lightHelper3.update(); + + renderer.render(scene, camera); +} + +init(); +updateTweens(); diff --git a/examples-testing/examples/webgl_lines_colors.ts b/examples-testing/examples/webgl_lines_colors.ts new file mode 100644 index 000000000..9da19ee2e --- /dev/null +++ b/examples-testing/examples/webgl_lines_colors.ts @@ -0,0 +1,181 @@ +import * as THREE from 'three'; + +import * as GeometryUtils from 'three/addons/utils/GeometryUtils.js'; + +let mouseX = 0, + mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +let camera, scene, renderer; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(33, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 1000; + + scene = new THREE.Scene(); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + const hilbertPoints = GeometryUtils.hilbert3D(new THREE.Vector3(0, 0, 0), 200.0, 1, 0, 1, 2, 3, 4, 5, 6, 7); + + const geometry1 = new THREE.BufferGeometry(); + const geometry2 = new THREE.BufferGeometry(); + const geometry3 = new THREE.BufferGeometry(); + + const subdivisions = 6; + + let vertices = []; + let colors1 = []; + let colors2 = []; + let colors3 = []; + + const point = new THREE.Vector3(); + const color = new THREE.Color(); + + const spline = new THREE.CatmullRomCurve3(hilbertPoints); + + for (let i = 0; i < hilbertPoints.length * subdivisions; i++) { + const t = i / (hilbertPoints.length * subdivisions); + spline.getPoint(t, point); + + vertices.push(point.x, point.y, point.z); + + color.setHSL(0.6, 1.0, Math.max(0, -point.x / 200) + 0.5, THREE.SRGBColorSpace); + colors1.push(color.r, color.g, color.b); + + color.setHSL(0.9, 1.0, Math.max(0, -point.y / 200) + 0.5, THREE.SRGBColorSpace); + colors2.push(color.r, color.g, color.b); + + color.setHSL(i / (hilbertPoints.length * subdivisions), 1.0, 0.5, THREE.SRGBColorSpace); + colors3.push(color.r, color.g, color.b); + } + + geometry1.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + geometry2.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + geometry3.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + + geometry1.setAttribute('color', new THREE.Float32BufferAttribute(colors1, 3)); + geometry2.setAttribute('color', new THREE.Float32BufferAttribute(colors2, 3)); + geometry3.setAttribute('color', new THREE.Float32BufferAttribute(colors3, 3)); + + // + + const geometry4 = new THREE.BufferGeometry(); + const geometry5 = new THREE.BufferGeometry(); + const geometry6 = new THREE.BufferGeometry(); + + vertices = []; + colors1 = []; + colors2 = []; + colors3 = []; + + for (let i = 0; i < hilbertPoints.length; i++) { + const point = hilbertPoints[i]; + + vertices.push(point.x, point.y, point.z); + + color.setHSL(0.6, 1.0, Math.max(0, (200 - hilbertPoints[i].x) / 400) * 0.5 + 0.5, THREE.SRGBColorSpace); + colors1.push(color.r, color.g, color.b); + + color.setHSL(0.3, 1.0, Math.max(0, (200 + hilbertPoints[i].x) / 400) * 0.5, THREE.SRGBColorSpace); + colors2.push(color.r, color.g, color.b); + + color.setHSL(i / hilbertPoints.length, 1.0, 0.5, THREE.SRGBColorSpace); + colors3.push(color.r, color.g, color.b); + } + + geometry4.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + geometry5.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + geometry6.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + + geometry4.setAttribute('color', new THREE.Float32BufferAttribute(colors1, 3)); + geometry5.setAttribute('color', new THREE.Float32BufferAttribute(colors2, 3)); + geometry6.setAttribute('color', new THREE.Float32BufferAttribute(colors3, 3)); + + // Create lines and add to scene + + const material = new THREE.LineBasicMaterial({ color: 0xffffff, vertexColors: true }); + + let line, p; + const scale = 0.3, + d = 225; + + const parameters = [ + [material, scale * 1.5, [-d, -d / 2, 0], geometry1], + [material, scale * 1.5, [0, -d / 2, 0], geometry2], + [material, scale * 1.5, [d, -d / 2, 0], geometry3], + + [material, scale * 1.5, [-d, d / 2, 0], geometry4], + [material, scale * 1.5, [0, d / 2, 0], geometry5], + [material, scale * 1.5, [d, d / 2, 0], geometry6], + ]; + + for (let i = 0; i < parameters.length; i++) { + p = parameters[i]; + line = new THREE.Line(p[3], p[0]); + line.scale.x = line.scale.y = line.scale.z = p[1]; + line.position.x = p[2][0]; + line.position.y = p[2][1]; + line.position.z = p[2][2]; + scene.add(line); + } + + // + + document.body.style.touchAction = 'none'; + document.body.addEventListener('pointermove', onPointerMove); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function onPointerMove(event) { + if (event.isPrimary === false) return; + + mouseX = event.clientX - windowHalfX; + mouseY = event.clientY - windowHalfY; +} + +// + +function animate() { + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-mouseY + 200 - camera.position.y) * 0.05; + + camera.lookAt(scene.position); + + const time = Date.now() * 0.0005; + + for (let i = 0; i < scene.children.length; i++) { + const object = scene.children[i]; + + if (object.isLine) { + object.rotation.y = time * (i % 2 ? 1 : -1); + } + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_lines_dashed.ts b/examples-testing/examples/webgl_lines_dashed.ts new file mode 100644 index 000000000..4849e7c3a --- /dev/null +++ b/examples-testing/examples/webgl_lines_dashed.ts @@ -0,0 +1,186 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import * as GeometryUtils from 'three/addons/utils/GeometryUtils.js'; + +let renderer, scene, camera, stats; +const objects = []; + +const WIDTH = window.innerWidth, + HEIGHT = window.innerHeight; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(60, WIDTH / HEIGHT, 1, 200); + camera.position.z = 150; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x111111); + scene.fog = new THREE.Fog(0x111111, 150, 200); + + const subdivisions = 6; + const recursion = 1; + + const points = GeometryUtils.hilbert3D(new THREE.Vector3(0, 0, 0), 25.0, recursion, 0, 1, 2, 3, 4, 5, 6, 7); + const spline = new THREE.CatmullRomCurve3(points); + + const samples = spline.getPoints(points.length * subdivisions); + const geometrySpline = new THREE.BufferGeometry().setFromPoints(samples); + + const line = new THREE.Line( + geometrySpline, + new THREE.LineDashedMaterial({ color: 0xffffff, dashSize: 1, gapSize: 0.5 }), + ); + line.computeLineDistances(); + + objects.push(line); + scene.add(line); + + const geometryBox = box(50, 50, 50); + + const lineSegments = new THREE.LineSegments( + geometryBox, + new THREE.LineDashedMaterial({ color: 0xffaa00, dashSize: 3, gapSize: 1 }), + ); + lineSegments.computeLineDistances(); + + objects.push(lineSegments); + scene.add(lineSegments); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(WIDTH, HEIGHT); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function box(width, height, depth) { + (width = width * 0.5), (height = height * 0.5), (depth = depth * 0.5); + + const geometry = new THREE.BufferGeometry(); + const position = []; + + position.push( + -width, + -height, + -depth, + -width, + height, + -depth, + + -width, + height, + -depth, + width, + height, + -depth, + + width, + height, + -depth, + width, + -height, + -depth, + + width, + -height, + -depth, + -width, + -height, + -depth, + + -width, + -height, + depth, + -width, + height, + depth, + + -width, + height, + depth, + width, + height, + depth, + + width, + height, + depth, + width, + -height, + depth, + + width, + -height, + depth, + -width, + -height, + depth, + + -width, + -height, + -depth, + -width, + -height, + depth, + + -width, + height, + -depth, + -width, + height, + depth, + + width, + height, + -depth, + width, + height, + depth, + + width, + -height, + -depth, + width, + -height, + depth, + ); + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(position, 3)); + + return geometry; +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + const time = Date.now() * 0.001; + + scene.traverse(function (object) { + if (object.isLine) { + object.rotation.x = 0.25 * time; + object.rotation.y = 0.25 * time; + } + }); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_lines_fat.ts b/examples-testing/examples/webgl_lines_fat.ts new file mode 100644 index 000000000..34c2e2d5e --- /dev/null +++ b/examples-testing/examples/webgl_lines_fat.ts @@ -0,0 +1,251 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GPUStatsPanel } from 'three/addons/utils/GPUStatsPanel.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { Line2 } from 'three/addons/lines/Line2.js'; +import { LineMaterial } from 'three/addons/lines/LineMaterial.js'; +import { LineGeometry } from 'three/addons/lines/LineGeometry.js'; +import * as GeometryUtils from 'three/addons/utils/GeometryUtils.js'; + +let line, renderer, scene, camera, camera2, controls; +let line1; +let matLine, matLineBasic, matLineDashed; +let stats, gpuPanel; +let gui; + +// viewport +let insetWidth; +let insetHeight; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setClearColor(0x000000, 0.0); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(-40, 0, 60); + + camera2 = new THREE.PerspectiveCamera(40, 1, 1, 1000); + camera2.position.copy(camera.position); + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.minDistance = 10; + controls.maxDistance = 500; + + // Position and THREE.Color Data + + const positions = []; + const colors = []; + + const points = GeometryUtils.hilbert3D(new THREE.Vector3(0, 0, 0), 20.0, 1, 0, 1, 2, 3, 4, 5, 6, 7); + + const spline = new THREE.CatmullRomCurve3(points); + const divisions = Math.round(12 * points.length); + const point = new THREE.Vector3(); + const color = new THREE.Color(); + + for (let i = 0, l = divisions; i < l; i++) { + const t = i / l; + + spline.getPoint(t, point); + positions.push(point.x, point.y, point.z); + + color.setHSL(t, 1.0, 0.5, THREE.SRGBColorSpace); + colors.push(color.r, color.g, color.b); + } + + // Line2 ( LineGeometry, LineMaterial ) + + const geometry = new LineGeometry(); + geometry.setPositions(positions); + geometry.setColors(colors); + + matLine = new LineMaterial({ + color: 0xffffff, + linewidth: 5, // in world units with size attenuation, pixels otherwise + vertexColors: true, + + dashed: false, + alphaToCoverage: true, + }); + + line = new Line2(geometry, matLine); + line.computeLineDistances(); + line.scale.set(1, 1, 1); + scene.add(line); + + // THREE.Line ( THREE.BufferGeometry, THREE.LineBasicMaterial ) - rendered with gl.LINE_STRIP + + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + + matLineBasic = new THREE.LineBasicMaterial({ vertexColors: true }); + matLineDashed = new THREE.LineDashedMaterial({ vertexColors: true, scale: 2, dashSize: 1, gapSize: 1 }); + + line1 = new THREE.Line(geo, matLineBasic); + line1.computeLineDistances(); + line1.visible = false; + scene.add(line1); + + // + + window.addEventListener('resize', onWindowResize); + onWindowResize(); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + gpuPanel = new GPUStatsPanel(renderer.getContext()); + stats.addPanel(gpuPanel); + stats.showPanel(0); + + initGui(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + insetWidth = window.innerHeight / 4; // square + insetHeight = window.innerHeight / 4; + + camera2.aspect = insetWidth / insetHeight; + camera2.updateProjectionMatrix(); +} + +function animate() { + // main scene + + renderer.setClearColor(0x000000, 0); + + renderer.setViewport(0, 0, window.innerWidth, window.innerHeight); + + controls.update(); + + gpuPanel.startQuery(); + renderer.render(scene, camera); + gpuPanel.endQuery(); + + // inset scene + + renderer.setClearColor(0x222222, 1); + + renderer.clearDepth(); // important! + + renderer.setScissorTest(true); + + renderer.setScissor(20, 20, insetWidth, insetHeight); + + renderer.setViewport(20, 20, insetWidth, insetHeight); + + camera2.position.copy(camera.position); + camera2.quaternion.copy(camera.quaternion); + + renderer.render(scene, camera2); + + renderer.setScissorTest(false); + + stats.update(); +} + +// + +function initGui() { + gui = new GUI(); + + const param = { + 'line type': 0, + 'world units': false, + width: 5, + alphaToCoverage: true, + dashed: false, + 'dash scale': 1, + 'dash / gap': 1, + }; + + gui.add(param, 'line type', { LineGeometry: 0, 'gl.LINE': 1 }).onChange(function (val) { + switch (val) { + case 0: + line.visible = true; + + line1.visible = false; + + break; + + case 1: + line.visible = false; + + line1.visible = true; + + break; + } + }); + + gui.add(param, 'world units').onChange(function (val) { + matLine.worldUnits = val; + matLine.needsUpdate = true; + }); + + gui.add(param, 'width', 1, 10).onChange(function (val) { + matLine.linewidth = val; + }); + + gui.add(param, 'alphaToCoverage').onChange(function (val) { + matLine.alphaToCoverage = val; + }); + + gui.add(param, 'dashed').onChange(function (val) { + matLine.dashed = val; + line1.material = val ? matLineDashed : matLineBasic; + }); + + gui.add(param, 'dash scale', 0.5, 2, 0.1).onChange(function (val) { + matLine.dashScale = val; + matLineDashed.scale = val; + }); + + gui.add(param, 'dash / gap', { '2 : 1': 0, '1 : 1': 1, '1 : 2': 2 }).onChange(function (val) { + switch (val) { + case 0: + matLine.dashSize = 2; + matLine.gapSize = 1; + + matLineDashed.dashSize = 2; + matLineDashed.gapSize = 1; + + break; + + case 1: + matLine.dashSize = 1; + matLine.gapSize = 1; + + matLineDashed.dashSize = 1; + matLineDashed.gapSize = 1; + + break; + + case 2: + matLine.dashSize = 1; + matLine.gapSize = 2; + + matLineDashed.dashSize = 1; + matLineDashed.gapSize = 2; + + break; + } + }); +} diff --git a/examples-testing/examples/webgl_lines_fat_raycasting.ts b/examples-testing/examples/webgl_lines_fat_raycasting.ts new file mode 100644 index 000000000..75cef6204 --- /dev/null +++ b/examples-testing/examples/webgl_lines_fat_raycasting.ts @@ -0,0 +1,294 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GPUStatsPanel } from 'three/addons/utils/GPUStatsPanel.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { LineMaterial } from 'three/addons/lines/LineMaterial.js'; +import { LineSegments2 } from 'three/addons/lines/LineSegments2.js'; +import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js'; +import { Line2 } from 'three/addons/lines/Line2.js'; +import { LineGeometry } from 'three/addons/lines/LineGeometry.js'; + +let line, thresholdLine, segments, thresholdSegments; +let renderer, scene, camera, controls; +let sphereInter, sphereOnLine; +let stats, gpuPanel; +let gui; +let clock; + +const color = new THREE.Color(); + +const pointer = new THREE.Vector2(Infinity, Infinity); + +const raycaster = new THREE.Raycaster(); + +raycaster.params.Line2 = {}; +raycaster.params.Line2.threshold = 0; + +const matLine = new LineMaterial({ + color: 0xffffff, + linewidth: 1, // in world units with size attenuation, pixels otherwise + worldUnits: true, + vertexColors: true, + + alphaToCoverage: true, +}); + +const matThresholdLine = new LineMaterial({ + color: 0xffffff, + linewidth: matLine.linewidth, // in world units with size attenuation, pixels otherwise + worldUnits: true, + // vertexColors: true, + transparent: true, + opacity: 0.2, + depthTest: false, + visible: false, +}); + +const params = { + 'line type': 0, + 'world units': matLine.worldUnits, + 'visualize threshold': matThresholdLine.visible, + width: matLine.linewidth, + alphaToCoverage: matLine.alphaToCoverage, + threshold: raycaster.params.Line2.threshold, + translation: raycaster.params.Line2.threshold, + animate: true, +}; + +init(); + +function init() { + clock = new THREE.Clock(); + + renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setClearColor(0x000000, 0.0); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(-40, 0, 60); + + controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 10; + controls.maxDistance = 500; + + const sphereGeometry = new THREE.SphereGeometry(0.25, 8, 4); + const sphereInterMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, depthTest: false }); + const sphereOnLineMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00, depthTest: false }); + + sphereInter = new THREE.Mesh(sphereGeometry, sphereInterMaterial); + sphereOnLine = new THREE.Mesh(sphereGeometry, sphereOnLineMaterial); + sphereInter.visible = false; + sphereOnLine.visible = false; + sphereInter.renderOrder = 10; + sphereOnLine.renderOrder = 10; + scene.add(sphereInter); + scene.add(sphereOnLine); + + // Position and THREE.Color Data + + const positions = []; + const colors = []; + const points = []; + for (let i = -50; i < 50; i++) { + const t = i / 3; + points.push(new THREE.Vector3(t * Math.sin(2 * t), t, t * Math.cos(2 * t))); + } + + const spline = new THREE.CatmullRomCurve3(points); + const divisions = Math.round(3 * points.length); + const point = new THREE.Vector3(); + const color = new THREE.Color(); + + for (let i = 0, l = divisions; i < l; i++) { + const t = i / l; + + spline.getPoint(t, point); + positions.push(point.x, point.y, point.z); + + color.setHSL(t, 1.0, 0.5, THREE.SRGBColorSpace); + colors.push(color.r, color.g, color.b); + } + + const lineGeometry = new LineGeometry(); + lineGeometry.setPositions(positions); + lineGeometry.setColors(colors); + + const segmentsGeometry = new LineSegmentsGeometry(); + segmentsGeometry.setPositions(positions); + segmentsGeometry.setColors(colors); + + segments = new LineSegments2(segmentsGeometry, matLine); + segments.computeLineDistances(); + segments.scale.set(1, 1, 1); + scene.add(segments); + segments.visible = false; + + thresholdSegments = new LineSegments2(segmentsGeometry, matThresholdLine); + thresholdSegments.computeLineDistances(); + thresholdSegments.scale.set(1, 1, 1); + scene.add(thresholdSegments); + thresholdSegments.visible = false; + + line = new Line2(lineGeometry, matLine); + line.computeLineDistances(); + line.scale.set(1, 1, 1); + scene.add(line); + + thresholdLine = new Line2(lineGeometry, matThresholdLine); + thresholdLine.computeLineDistances(); + thresholdLine.scale.set(1, 1, 1); + scene.add(thresholdLine); + + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); + + // + + document.addEventListener('pointermove', onPointerMove); + window.addEventListener('resize', onWindowResize); + onWindowResize(); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + gpuPanel = new GPUStatsPanel(renderer.getContext()); + stats.addPanel(gpuPanel); + stats.showPanel(0); + initGui(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onPointerMove(event) { + pointer.x = (event.clientX / window.innerWidth) * 2 - 1; + pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; +} + +function animate() { + const delta = clock.getDelta(); + + const obj = line.visible ? line : segments; + thresholdLine.position.copy(line.position); + thresholdLine.quaternion.copy(line.quaternion); + thresholdSegments.position.copy(segments.position); + thresholdSegments.quaternion.copy(segments.quaternion); + + if (params.animate) { + line.rotation.y += delta * 0.1; + + segments.rotation.y = line.rotation.y; + } + + raycaster.setFromCamera(pointer, camera); + + const intersects = raycaster.intersectObject(obj); + + if (intersects.length > 0) { + sphereInter.visible = true; + sphereOnLine.visible = true; + + sphereInter.position.copy(intersects[0].point); + sphereOnLine.position.copy(intersects[0].pointOnLine); + + const index = intersects[0].faceIndex; + const colors = obj.geometry.getAttribute('instanceColorStart'); + + color.fromBufferAttribute(colors, index); + + sphereInter.material.color.copy(color).offsetHSL(0.3, 0, 0); + sphereOnLine.material.color.copy(color).offsetHSL(0.7, 0, 0); + + renderer.domElement.style.cursor = 'crosshair'; + } else { + sphereInter.visible = false; + sphereOnLine.visible = false; + renderer.domElement.style.cursor = ''; + } + + gpuPanel.startQuery(); + renderer.render(scene, camera); + gpuPanel.endQuery(); + + stats.update(); +} + +// + +function switchLine(val) { + switch (val) { + case 0: + line.visible = true; + thresholdLine.visible = true; + + segments.visible = false; + thresholdSegments.visible = false; + + break; + + case 1: + line.visible = false; + thresholdLine.visible = false; + + segments.visible = true; + thresholdSegments.visible = true; + + break; + } +} + +function initGui() { + gui = new GUI(); + + gui.add(params, 'line type', { LineGeometry: 0, LineSegmentsGeometry: 1 }) + .onChange(function (val) { + switchLine(val); + }) + .setValue(1); + + gui.add(params, 'world units').onChange(function (val) { + matLine.worldUnits = val; + matLine.needsUpdate = true; + + matThresholdLine.worldUnits = val; + matThresholdLine.needsUpdate = true; + }); + + gui.add(params, 'visualize threshold').onChange(function (val) { + matThresholdLine.visible = val; + }); + + gui.add(params, 'width', 1, 10).onChange(function (val) { + matLine.linewidth = val; + matThresholdLine.linewidth = matLine.linewidth + raycaster.params.Line2.threshold; + }); + + gui.add(params, 'alphaToCoverage').onChange(function (val) { + matLine.alphaToCoverage = val; + }); + + gui.add(params, 'threshold', 0, 10).onChange(function (val) { + raycaster.params.Line2.threshold = val; + matThresholdLine.linewidth = matLine.linewidth + raycaster.params.Line2.threshold; + }); + + gui.add(params, 'translation', 0, 10).onChange(function (val) { + line.position.x = val; + segments.position.x = val; + }); + + gui.add(params, 'animate'); +} diff --git a/examples-testing/examples/webgl_lines_fat_wireframe.ts b/examples-testing/examples/webgl_lines_fat_wireframe.ts new file mode 100644 index 000000000..59660ad7e --- /dev/null +++ b/examples-testing/examples/webgl_lines_fat_wireframe.ts @@ -0,0 +1,210 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { LineMaterial } from 'three/addons/lines/LineMaterial.js'; +import { Wireframe } from 'three/addons/lines/Wireframe.js'; +import { WireframeGeometry2 } from 'three/addons/lines/WireframeGeometry2.js'; + +let wireframe, renderer, scene, camera, camera2, controls; +let wireframe1; +let matLine, matLineBasic, matLineDashed; +let stats; +let gui; + +// viewport +let insetWidth; +let insetHeight; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setClearColor(0x000000, 0.0); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(-50, 0, 50); + + camera2 = new THREE.PerspectiveCamera(40, 1, 1, 1000); + camera2.position.copy(camera.position); + + controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 10; + controls.maxDistance = 500; + + // Wireframe ( WireframeGeometry2, LineMaterial ) + + let geo = new THREE.IcosahedronGeometry(20, 1); + + const geometry = new WireframeGeometry2(geo); + + matLine = new LineMaterial({ + color: 0x4080ff, + linewidth: 5, // in pixels + dashed: false, + }); + + wireframe = new Wireframe(geometry, matLine); + wireframe.computeLineDistances(); + wireframe.scale.set(1, 1, 1); + scene.add(wireframe); + + // Line ( THREE.WireframeGeometry, THREE.LineBasicMaterial ) - rendered with gl.LINE + + geo = new THREE.WireframeGeometry(geo); + + matLineBasic = new THREE.LineBasicMaterial({ color: 0x4080ff }); + matLineDashed = new THREE.LineDashedMaterial({ scale: 2, dashSize: 1, gapSize: 1 }); + + wireframe1 = new THREE.LineSegments(geo, matLineBasic); + wireframe1.computeLineDistances(); + wireframe1.visible = false; + scene.add(wireframe1); + + // + + window.addEventListener('resize', onWindowResize); + onWindowResize(); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + initGui(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + insetWidth = window.innerHeight / 4; // square + insetHeight = window.innerHeight / 4; + + camera2.aspect = insetWidth / insetHeight; + camera2.updateProjectionMatrix(); +} + +function animate() { + // main scene + + renderer.setClearColor(0x000000, 0); + + renderer.setViewport(0, 0, window.innerWidth, window.innerHeight); + + renderer.render(scene, camera); + + // inset scene + + renderer.setClearColor(0x222222, 1); + + renderer.clearDepth(); // important! + + renderer.setScissorTest(true); + + renderer.setScissor(20, 20, insetWidth, insetHeight); + + renderer.setViewport(20, 20, insetWidth, insetHeight); + + camera2.position.copy(camera.position); + camera2.quaternion.copy(camera.quaternion); + + renderer.render(scene, camera2); + + renderer.setScissorTest(false); + + stats.update(); +} + +// + +function initGui() { + gui = new GUI(); + + const param = { + 'line type': 0, + 'width (px)': 5, + dashed: false, + 'dash scale': 1, + 'dash / gap': 1, + }; + + gui.add(param, 'line type', { LineGeometry: 0, 'gl.LINE': 1 }).onChange(function (val) { + switch (val) { + case 0: + wireframe.visible = true; + + wireframe1.visible = false; + + break; + + case 1: + wireframe.visible = false; + + wireframe1.visible = true; + + break; + } + }); + + gui.add(param, 'width (px)', 1, 10).onChange(function (val) { + matLine.linewidth = val; + }); + + gui.add(param, 'dashed').onChange(function (val) { + matLine.dashed = val; + + // dashed is implemented as a defines -- not as a uniform. this could be changed. + // ... or THREE.LineDashedMaterial could be implemented as a separate material + // temporary hack - renderer should do this eventually + if (val) matLine.defines.USE_DASH = ''; + else delete matLine.defines.USE_DASH; + matLine.needsUpdate = true; + + wireframe1.material = val ? matLineDashed : matLineBasic; + }); + + gui.add(param, 'dash scale', 0.5, 1, 0.1).onChange(function (val) { + matLine.dashScale = val; + matLineDashed.scale = val; + }); + + gui.add(param, 'dash / gap', { '2 : 1': 0, '1 : 1': 1, '1 : 2': 2 }).onChange(function (val) { + switch (val) { + case 0: + matLine.dashSize = 2; + matLine.gapSize = 1; + + matLineDashed.dashSize = 2; + matLineDashed.gapSize = 1; + + break; + + case 1: + matLine.dashSize = 1; + matLine.gapSize = 1; + + matLineDashed.dashSize = 1; + matLineDashed.gapSize = 1; + + break; + + case 2: + matLine.dashSize = 1; + matLine.gapSize = 2; + + matLineDashed.dashSize = 1; + matLineDashed.gapSize = 2; + + break; + } + }); +} diff --git a/examples-testing/examples/webgl_loader_3dm.ts b/examples-testing/examples/webgl_loader_3dm.ts new file mode 100644 index 000000000..7570306fd --- /dev/null +++ b/examples-testing/examples/webgl_loader_3dm.ts @@ -0,0 +1,95 @@ +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(); + +function init() { + THREE.Object3D.DEFAULT_UP.set(0, 0, 1); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(26, -40, 5); + + scene = new THREE.Scene(); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 6); + directionalLight.position.set(0, 0, 2); + scene.add(directionalLight); + + const loader = new Rhino3dmLoader(); + //generally, use this for the Library Path: https://cdn.jsdelivr.net/npm/rhino3dm@8.0.1 + loader.setLibraryPath('jsm/libs/rhino3dm/'); + loader.load( + 'models/3dm/Rhino_Logo.3dm', + function (object) { + scene.add(object); + initGUI(object.userData.layers); + + // hide spinner + document.getElementById('loader').style.display = 'none'; + }, + function (progress) { + console.log((progress.loaded / progress.total) * 100 + '%'); + }, + function (error) { + console.log(error); + }, + ); + + controls = new OrbitControls(camera, renderer.domElement); + + 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); +} + +function initGUI(layers) { + gui = new GUI({ title: 'layers' }); + + 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; + } + } + } + }); + }); + } +} diff --git a/examples-testing/examples/webgl_loader_3ds.ts b/examples-testing/examples/webgl_loader_3ds.ts new file mode 100644 index 000000000..10ce34076 --- /dev/null +++ b/examples-testing/examples/webgl_loader_3ds.ts @@ -0,0 +1,62 @@ +import * as THREE from 'three'; + +import { TrackballControls } from 'three/addons/controls/TrackballControls.js'; +import { TDSLoader } from 'three/addons/loaders/TDSLoader.js'; + +let container, controls; +let camera, scene, renderer; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 10); + camera.position.z = 2; + + scene = new THREE.Scene(); + scene.add(new THREE.AmbientLight(0xffffff, 3)); + + const directionalLight = new THREE.DirectionalLight(0xffeedd, 3); + directionalLight.position.set(0, 0, 2); + scene.add(directionalLight); + + //3ds files dont store normal maps + const normal = new THREE.TextureLoader().load('models/3ds/portalgun/textures/normal.jpg'); + + const loader = new TDSLoader(); + loader.setResourcePath('models/3ds/portalgun/textures/'); + loader.load('models/3ds/portalgun/portalgun.3ds', function (object) { + object.traverse(function (child) { + if (child.isMesh) { + child.material.specular.setScalar(0.1); + child.material.normalMap = normal; + } + }); + + scene.add(object); + }); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + controls = new TrackballControls(camera, renderer.domElement); + + window.addEventListener('resize', resize); +} + +function resize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_3mf.ts b/examples-testing/examples/webgl_loader_3mf.ts new file mode 100644 index 000000000..c31e32196 --- /dev/null +++ b/examples-testing/examples/webgl_loader_3mf.ts @@ -0,0 +1,105 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { ThreeMFLoader } from 'three/addons/loaders/3MFLoader.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, object, loader, controls; + +const params = { + asset: 'cube_gears', +}; + +const assets = ['cube_gears', 'facecolors', 'multipletextures', 'vertexcolors']; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x333333); + + camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 500); + + // Z is up for objects intended to be 3D printed. + + camera.up.set(0, 0, 1); + camera.position.set(-100, -250, 100); + scene.add(camera); + + controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); + controls.minDistance = 50; + controls.maxDistance = 400; + controls.enablePan = false; + controls.update(); + + scene.add(new THREE.AmbientLight(0xffffff, 0.6)); + + const light = new THREE.DirectionalLight(0xffffff, 2); + light.position.set(-1, -2.5, 1); + scene.add(light); + + const manager = new THREE.LoadingManager(); + + manager.onLoad = function () { + const aabb = new THREE.Box3().setFromObject(object); + const center = aabb.getCenter(new THREE.Vector3()); + + object.position.x += object.position.x - center.x; + object.position.y += object.position.y - center.y; + object.position.z += object.position.z - center.z; + + controls.reset(); + + scene.add(object); + render(); + }; + + loader = new ThreeMFLoader(manager); + loadAsset(params.asset); + + window.addEventListener('resize', onWindowResize); + + // + + const gui = new GUI(); + gui.add(params, 'asset', assets).onChange(function (value) { + loadAsset(value); + }); +} + +function loadAsset(asset) { + loader.load('models/3mf/' + asset + '.3mf', function (group) { + if (object) { + object.traverse(function (child) { + if (child.material) child.material.dispose(); + if (child.material && child.material.map) child.material.map.dispose(); + if (child.geometry) child.geometry.dispose(); + }); + + scene.remove(object); + } + + // + + object = group; + }); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_3mf_materials.ts b/examples-testing/examples/webgl_loader_3mf_materials.ts new file mode 100644 index 000000000..fcdd7308e --- /dev/null +++ b/examples-testing/examples/webgl_loader_3mf_materials.ts @@ -0,0 +1,106 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { ThreeMFLoader } from 'three/addons/loaders/3MFLoader.js'; + +let camera, scene, renderer; + +init(); + +function init() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xa0a0a0); + scene.fog = new THREE.Fog(0xa0a0a0, 10, 500); + + camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 500); + camera.position.set(-50, 40, 50); + scene.add(camera); + + // + + const hemiLight = new THREE.HemisphereLight(0xffffff, 0x8d8d8d, 3); + hemiLight.position.set(0, 100, 0); + scene.add(hemiLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.position.set(-0, 40, 50); + dirLight.castShadow = true; + dirLight.shadow.camera.top = 50; + dirLight.shadow.camera.bottom = -25; + dirLight.shadow.camera.left = -25; + dirLight.shadow.camera.right = 25; + dirLight.shadow.camera.near = 0.1; + dirLight.shadow.camera.far = 200; + dirLight.shadow.mapSize.set(1024, 1024); + scene.add(dirLight); + + // scene.add( new THREE.CameraHelper( dirLight.shadow.camera ) ); + + // + + const manager = new THREE.LoadingManager(); + + const loader = new ThreeMFLoader(manager); + loader.load('./models/3mf/truck.3mf', function (object) { + object.rotation.set(-Math.PI / 2, 0, 0); // z-up conversion + + object.traverse(function (child) { + child.castShadow = true; + }); + + scene.add(object); + }); + + // + + manager.onLoad = function () { + render(); + }; + + // + + const ground = new THREE.Mesh( + new THREE.PlaneGeometry(1000, 1000), + new THREE.MeshPhongMaterial({ color: 0xcbcbcb, depthWrite: false }), + ); + ground.rotation.x = -Math.PI / 2; + ground.position.y = 11; + ground.receiveShadow = true; + scene.add(ground); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.PCFSoftShadowMap; + document.body.appendChild(renderer.domElement); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); + controls.minDistance = 50; + controls.maxDistance = 200; + controls.enablePan = false; + controls.target.set(0, 20, 0); + controls.update(); + + window.addEventListener('resize', onWindowResize); + + render(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_amf.ts b/examples-testing/examples/webgl_loader_amf.ts new file mode 100644 index 000000000..ee576e04f --- /dev/null +++ b/examples-testing/examples/webgl_loader_amf.ts @@ -0,0 +1,62 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { AMFLoader } from 'three/addons/loaders/AMFLoader.js'; + +let camera, scene, renderer; + +init(); + +function init() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x999999); + + scene.add(new THREE.AmbientLight(0x999999)); + + camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 500); + + // Z is up for objects intended to be 3D printed. + + camera.up.set(0, 0, 1); + camera.position.set(0, -9, 6); + + camera.add(new THREE.PointLight(0xffffff, 250)); + + scene.add(camera); + + const grid = new THREE.GridHelper(50, 50, 0xffffff, 0x555555); + grid.rotateOnAxis(new THREE.Vector3(1, 0, 0), 90 * (Math.PI / 180)); + scene.add(grid); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + const loader = new AMFLoader(); + loader.load('./models/amf/rook.amf', function (amfobject) { + scene.add(amfobject); + render(); + }); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); + controls.target.set(0, 0, 2); + controls.enableZoom = false; + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_bvh.ts b/examples-testing/examples/webgl_loader_bvh.ts new file mode 100644 index 000000000..0be3add4d --- /dev/null +++ b/examples-testing/examples/webgl_loader_bvh.ts @@ -0,0 +1,61 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { BVHLoader } from 'three/addons/loaders/BVHLoader.js'; + +const clock = new THREE.Clock(); + +let camera, controls, scene, renderer; +let mixer; + +init(); + +const loader = new BVHLoader(); +loader.load('models/bvh/pirouette.bvh', function (result) { + const skeletonHelper = new THREE.SkeletonHelper(result.skeleton.bones[0]); + + scene.add(result.skeleton.bones[0]); + scene.add(skeletonHelper); + + // play animation + mixer = new THREE.AnimationMixer(result.skeleton.bones[0]); + mixer.clipAction(result.clip).play(); +}); + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 200, 300); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xeeeeee); + + scene.add(new THREE.GridHelper(400, 10)); + + // renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 300; + controls.maxDistance = 700; + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const delta = clock.getDelta(); + + if (mixer) mixer.update(delta); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_collada.ts b/examples-testing/examples/webgl_loader_collada.ts new file mode 100644 index 000000000..62588b698 --- /dev/null +++ b/examples-testing/examples/webgl_loader_collada.ts @@ -0,0 +1,83 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { ColladaLoader } from 'three/addons/loaders/ColladaLoader.js'; + +let container, stats, clock; +let camera, scene, renderer, elf; + +init(); + +function init() { + container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 2000); + camera.position.set(8, 10, 8); + camera.lookAt(0, 3, 0); + + scene = new THREE.Scene(); + + clock = new THREE.Clock(); + + // loading manager + + const loadingManager = new THREE.LoadingManager(function () { + scene.add(elf); + }); + + // collada + + const loader = new ColladaLoader(loadingManager); + loader.load('./models/collada/elf/elf.dae', function (collada) { + elf = collada.scene; + }); + + // + + const ambientLight = new THREE.AmbientLight(0xffffff); + scene.add(ambientLight); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 2.5); + directionalLight.position.set(1, 1, 0).normalize(); + scene.add(directionalLight); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + const delta = clock.getDelta(); + + if (elf !== undefined) { + elf.rotation.z += delta * 0.5; + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_collada_skinning.ts b/examples-testing/examples/webgl_loader_collada_skinning.ts new file mode 100644 index 000000000..5cb808b17 --- /dev/null +++ b/examples-testing/examples/webgl_loader_collada_skinning.ts @@ -0,0 +1,97 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { ColladaLoader } from 'three/addons/loaders/ColladaLoader.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let container, stats, clock, controls; +let camera, scene, renderer, mixer; + +init(); + +function init() { + container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(25, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(15, 10, -15); + + scene = new THREE.Scene(); + + clock = new THREE.Clock(); + + // collada + + const loader = new ColladaLoader(); + loader.load('./models/collada/stormtrooper/stormtrooper.dae', function (collada) { + const avatar = collada.scene; + const animations = avatar.animations; + + mixer = new THREE.AnimationMixer(avatar); + mixer.clipAction(animations[0]).play(); + + scene.add(avatar); + }); + + // + + const gridHelper = new THREE.GridHelper(10, 20, 0xc1c1c1, 0x8d8d8d); + scene.add(gridHelper); + + // + + const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); + scene.add(ambientLight); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 3); + directionalLight.position.set(1.5, 1, -1.5); + scene.add(directionalLight); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + controls = new OrbitControls(camera, renderer.domElement); + controls.screenSpacePanning = true; + controls.minDistance = 5; + controls.maxDistance = 40; + controls.target.set(0, 2, 0); + controls.update(); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + const delta = clock.getDelta(); + + if (mixer !== undefined) { + mixer.update(delta); + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_draco.ts b/examples-testing/examples/webgl_loader_draco.ts new file mode 100644 index 000000000..c9947c693 --- /dev/null +++ b/examples-testing/examples/webgl_loader_draco.ts @@ -0,0 +1,85 @@ +import * as THREE from 'three'; + +import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; + +let camera, scene, renderer; + +const container = document.querySelector('#container'); + +// Configure and create Draco decoder. +const dracoLoader = new DRACOLoader(); +dracoLoader.setDecoderPath('jsm/libs/draco/'); +dracoLoader.setDecoderConfig({ type: 'js' }); + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 15); + camera.position.set(3, 0.25, 3); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x443333); + scene.fog = new THREE.Fog(0x443333, 1, 4); + + // Ground + const plane = new THREE.Mesh( + new THREE.PlaneGeometry(8, 8), + new THREE.MeshPhongMaterial({ color: 0xcbcbcb, specular: 0x101010 }), + ); + plane.rotation.x = -Math.PI / 2; + plane.position.y = 0.03; + plane.receiveShadow = true; + scene.add(plane); + + // Lights + const hemiLight = new THREE.HemisphereLight(0x8d7c7c, 0x494966, 3); + scene.add(hemiLight); + + const spotLight = new THREE.SpotLight(); + spotLight.intensity = 7; + spotLight.angle = Math.PI / 16; + spotLight.penumbra = 0.5; + spotLight.castShadow = true; + spotLight.position.set(-1, 1, 1); + scene.add(spotLight); + + dracoLoader.load('models/draco/bunny.drc', function (geometry) { + geometry.computeVertexNormals(); + + const material = new THREE.MeshStandardMaterial({ color: 0xa5a5a5 }); + const mesh = new THREE.Mesh(geometry, material); + mesh.castShadow = true; + mesh.receiveShadow = true; + scene.add(mesh); + + // Release decoder resources. + dracoLoader.dispose(); + }); + + // renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + container.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const timer = Date.now() * 0.0003; + + camera.position.x = Math.sin(timer) * 0.5; + camera.position.z = Math.cos(timer) * 0.5; + camera.lookAt(0, 0.1, 0); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_fbx.ts b/examples-testing/examples/webgl_loader_fbx.ts new file mode 100644 index 000000000..3b157a222 --- /dev/null +++ b/examples-testing/examples/webgl_loader_fbx.ts @@ -0,0 +1,162 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { FBXLoader } from 'three/addons/loaders/FBXLoader.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +const manager = new THREE.LoadingManager(); + +let camera, scene, renderer, stats, object, loader, guiMorphsFolder; +let mixer; + +const clock = new THREE.Clock(); + +const params = { + asset: 'Samba Dancing', +}; + +const assets = ['Samba Dancing', 'morph_test']; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000); + camera.position.set(100, 200, 300); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xa0a0a0); + scene.fog = new THREE.Fog(0xa0a0a0, 200, 1000); + + const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 5); + hemiLight.position.set(0, 200, 0); + scene.add(hemiLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 5); + dirLight.position.set(0, 200, 100); + dirLight.castShadow = true; + dirLight.shadow.camera.top = 180; + dirLight.shadow.camera.bottom = -100; + dirLight.shadow.camera.left = -120; + dirLight.shadow.camera.right = 120; + scene.add(dirLight); + + // scene.add( new THREE.CameraHelper( dirLight.shadow.camera ) ); + + // ground + const mesh = new THREE.Mesh( + new THREE.PlaneGeometry(2000, 2000), + new THREE.MeshPhongMaterial({ color: 0x999999, depthWrite: false }), + ); + mesh.rotation.x = -Math.PI / 2; + mesh.receiveShadow = true; + scene.add(mesh); + + const grid = new THREE.GridHelper(2000, 20, 0x000000, 0x000000); + grid.material.opacity = 0.2; + grid.material.transparent = true; + scene.add(grid); + + loader = new FBXLoader(manager); + loadAsset(params.asset); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + container.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 100, 0); + controls.update(); + + window.addEventListener('resize', onWindowResize); + + // stats + stats = new Stats(); + container.appendChild(stats.dom); + + const gui = new GUI(); + gui.add(params, 'asset', assets).onChange(function (value) { + loadAsset(value); + }); + + guiMorphsFolder = gui.addFolder('Morphs').hide(); +} + +function loadAsset(asset) { + loader.load('models/fbx/' + asset + '.fbx', function (group) { + if (object) { + object.traverse(function (child) { + if (child.material) { + const materials = Array.isArray(child.material) ? child.material : [child.material]; + materials.forEach(material => { + if (material.map) material.map.dispose(); + material.dispose(); + }); + } + + if (child.geometry) child.geometry.dispose(); + }); + + scene.remove(object); + } + + // + + object = group; + + if (object.animations && object.animations.length) { + mixer = new THREE.AnimationMixer(object); + + const action = mixer.clipAction(object.animations[0]); + action.play(); + } else { + mixer = null; + } + + guiMorphsFolder.children.forEach(child => child.destroy()); + guiMorphsFolder.hide(); + + object.traverse(function (child) { + if (child.isMesh) { + child.castShadow = true; + child.receiveShadow = true; + + if (child.morphTargetDictionary) { + guiMorphsFolder.show(); + const meshFolder = guiMorphsFolder.addFolder(child.name || child.uuid); + Object.keys(child.morphTargetDictionary).forEach(key => { + meshFolder.add(child.morphTargetInfluences, child.morphTargetDictionary[key], 0, 1, 0.01); + }); + } + } + }); + + scene.add(object); + }); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const delta = clock.getDelta(); + + if (mixer) mixer.update(delta); + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_loader_fbx_nurbs.ts b/examples-testing/examples/webgl_loader_fbx_nurbs.ts new file mode 100644 index 000000000..f2e45bcb5 --- /dev/null +++ b/examples-testing/examples/webgl_loader_fbx_nurbs.ts @@ -0,0 +1,61 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { FBXLoader } from 'three/addons/loaders/FBXLoader.js'; + +let camera, scene, renderer, stats; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000); + camera.position.set(2, 18, 28); + + scene = new THREE.Scene(); + + // grid + const gridHelper = new THREE.GridHelper(28, 28, 0x303030, 0x303030); + scene.add(gridHelper); + + // stats + stats = new Stats(); + container.appendChild(stats.dom); + + // model + const loader = new FBXLoader(); + loader.load('models/fbx/nurbs.fbx', function (object) { + scene.add(object); + }); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 12, 0); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_loader_gcode.ts b/examples-testing/examples/webgl_loader_gcode.ts new file mode 100644 index 000000000..6fd3e149d --- /dev/null +++ b/examples-testing/examples/webgl_loader_gcode.ts @@ -0,0 +1,49 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GCodeLoader } from 'three/addons/loaders/GCodeLoader.js'; + +let camera, scene, renderer; + +init(); +render(); + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 0, 70); + + scene = new THREE.Scene(); + + const loader = new GCodeLoader(); + loader.load('models/gcode/benchy.gcode', function (object) { + object.position.set(-100, -20, 100); + scene.add(object); + + render(); + }); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use if there is no animation loop + controls.minDistance = 10; + controls.maxDistance = 100; + + window.addEventListener('resize', resize); +} + +function resize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_gltf.ts b/examples-testing/examples/webgl_loader_gltf.ts new file mode 100644 index 000000000..e1b0adc51 --- /dev/null +++ b/examples-testing/examples/webgl_loader_gltf.ts @@ -0,0 +1,74 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +let camera, scene, renderer; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 20); + camera.position.set(-1.8, 0.6, 2.7); + + scene = new THREE.Scene(); + + new RGBELoader().setPath('textures/equirectangular/').load('royal_esplanade_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + scene.environment = texture; + + render(); + + // model + + const loader = new GLTFLoader().setPath('models/gltf/DamagedHelmet/glTF/'); + loader.load('DamagedHelmet.gltf', async function (gltf) { + const model = gltf.scene; + + // wait until the model can be added to the scene without blocking due to shader compilation + + await renderer.compileAsync(model, camera, scene); + + scene.add(model); + + render(); + }); + }); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1; + container.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use if there is no animation loop + controls.minDistance = 2; + controls.maxDistance = 10; + controls.target.set(0, 0, -0.2); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +// + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_gltf_anisotropy.ts b/examples-testing/examples/webgl_loader_gltf_anisotropy.ts new file mode 100644 index 000000000..6e240a272 --- /dev/null +++ b/examples-testing/examples/webgl_loader_gltf_anisotropy.ts @@ -0,0 +1,68 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +let renderer, scene, camera, controls; + +init(); + +async function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1.35; + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.01, 10); + camera.position.set(-0.35, -0.2, 0.35); + + controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, -0.08, 0.11); + controls.minDistance = 0.1; + controls.maxDistance = 2; + controls.addEventListener('change', render); + controls.update(); + + const rgbeLoader = new RGBELoader().setPath('textures/equirectangular/'); + const gltfLoader = new GLTFLoader().setPath('models/gltf/'); + + const [texture, gltf] = await Promise.all([ + rgbeLoader.loadAsync('royal_esplanade_1k.hdr'), + gltfLoader.loadAsync('AnisotropyBarnLamp.glb'), + ]); + + // environment + + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + scene.backgroundBlurriness = 0.5; + scene.environment = texture; + + // model + + scene.add(gltf.scene); + + render(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_gltf_avif.ts b/examples-testing/examples/webgl_loader_gltf_avif.ts new file mode 100644 index 000000000..37d63859e --- /dev/null +++ b/examples-testing/examples/webgl_loader_gltf_avif.ts @@ -0,0 +1,61 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; + +let camera, scene, renderer; + +init(); +render(); + +function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(1.5, 4, 9); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf6eedc); + + // + + const dracoLoader = new DRACOLoader(); + dracoLoader.setDecoderPath('jsm/libs/draco/gltf/'); + + const loader = new GLTFLoader(); + loader.setDRACOLoader(dracoLoader); + loader.setPath('models/gltf/AVIFTest/'); + loader.load('forest_house.glb', function (gltf) { + scene.add(gltf.scene); + + render(); + }); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); + controls.target.set(0, 2, 0); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +// + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_gltf_compressed.ts b/examples-testing/examples/webgl_loader_gltf_compressed.ts new file mode 100644 index 000000000..3bdcea8ec --- /dev/null +++ b/examples-testing/examples/webgl_loader_gltf_compressed.ts @@ -0,0 +1,83 @@ +import * as THREE from 'three'; + +import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'; +import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'; + +let camera, scene, renderer; + +init(); +render(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1; + container.appendChild(renderer.domElement); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000); + camera.position.set(0, 100, 0); + + const environment = new RoomEnvironment(); + const pmremGenerator = new THREE.PMREMGenerator(renderer); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xbbbbbb); + scene.environment = pmremGenerator.fromScene(environment).texture; + environment.dispose(); + + const grid = new THREE.GridHelper(500, 10, 0xffffff, 0xffffff); + grid.material.opacity = 0.5; + grid.material.depthWrite = false; + grid.material.transparent = true; + scene.add(grid); + + const ktx2Loader = new KTX2Loader().setTranscoderPath('jsm/libs/basis/').detectSupport(renderer); + + const loader = new GLTFLoader().setPath('models/gltf/'); + loader.setKTX2Loader(ktx2Loader); + loader.setMeshoptDecoder(MeshoptDecoder); + loader.load('coffeemat.glb', function (gltf) { + // coffeemat.glb was produced from the source scene using gltfpack: + // gltfpack -i coffeemat/scene.gltf -o coffeemat.glb -cc -tc + // The resulting model uses EXT_meshopt_compression (for geometry) and KHR_texture_basisu (for texture compression using ETC1S/BasisLZ) + + gltf.scene.position.y = 8; + + scene.add(gltf.scene); + + render(); + }); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use if there is no animation loop + controls.minDistance = 400; + controls.maxDistance = 1000; + controls.target.set(10, 90, -16); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +// + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_gltf_dispersion.ts b/examples-testing/examples/webgl_loader_gltf_dispersion.ts new file mode 100644 index 000000000..100badcab --- /dev/null +++ b/examples-testing/examples/webgl_loader_gltf_dispersion.ts @@ -0,0 +1,66 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; + +let camera, scene, renderer; + +init().then(render); + +async function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 5); + camera.position.set(0.1, 0.05, 0.15); + + scene = new THREE.Scene(); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.toneMapping = THREE.NeutralToneMapping; + renderer.toneMappingExposure = 1; + container.appendChild(renderer.domElement); + + const environment = new RoomEnvironment(); + const pmremGenerator = new THREE.PMREMGenerator(renderer); + + scene = new THREE.Scene(); + scene.backgroundBlurriness = 0.5; + + const env = pmremGenerator.fromScene(environment).texture; + scene.background = env; + scene.environment = env; + environment.dispose(); + + const loader = new GLTFLoader(); + const gltf = await loader.loadAsync('models/gltf/DispersionTest.glb'); + + scene.add(gltf.scene); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use if there is no animation loop + controls.minDistance = 0.1; + controls.maxDistance = 10; + controls.target.set(0, 0, 0); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +// + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_gltf_instancing.ts b/examples-testing/examples/webgl_loader_gltf_instancing.ts new file mode 100644 index 000000000..5d23e7750 --- /dev/null +++ b/examples-testing/examples/webgl_loader_gltf_instancing.ts @@ -0,0 +1,69 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +let camera, scene, renderer; + +init(); +render(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 20); + camera.position.set(-0.9, 0.41, -0.89); + + scene = new THREE.Scene(); + + new RGBELoader().setPath('textures/equirectangular/').load('royal_esplanade_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + scene.environment = texture; + + render(); + + // model + + const loader = new GLTFLoader().setPath('models/gltf/DamagedHelmet/glTF-instancing/'); + loader.load('DamagedHelmetGpuInstancing.gltf', function (gltf) { + scene.add(gltf.scene); + + render(); + }); + }); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1; + container.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use if there is no animation loop + controls.minDistance = 0.2; + controls.maxDistance = 10; + controls.target.set(0, 0.25, 0); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +// + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_gltf_iridescence.ts b/examples-testing/examples/webgl_loader_gltf_iridescence.ts new file mode 100644 index 000000000..eb0f8d914 --- /dev/null +++ b/examples-testing/examples/webgl_loader_gltf_iridescence.ts @@ -0,0 +1,66 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +let renderer, scene, camera, controls; + +init().catch(function (err) { + console.error(err); +}); + +async function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setAnimationLoop(animate); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.05, 20); + camera.position.set(0.35, 0.05, 0.35); + + controls = new OrbitControls(camera, renderer.domElement); + controls.autoRotate = true; + controls.autoRotateSpeed = -0.5; + controls.target.set(0, 0.2, 0); + controls.update(); + + const rgbeLoader = new RGBELoader().setPath('textures/equirectangular/'); + + const gltfLoader = new GLTFLoader().setPath('models/gltf/'); + + const [texture, gltf] = await Promise.all([ + rgbeLoader.loadAsync('venice_sunset_1k.hdr'), + gltfLoader.loadAsync('IridescenceLamp.glb'), + ]); + + // environment + + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + scene.environment = texture; + + // model + + scene.add(gltf.scene); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_gltf_sheen.ts b/examples-testing/examples/webgl_loader_gltf_sheen.ts new file mode 100644 index 000000000..bd258d5c1 --- /dev/null +++ b/examples-testing/examples/webgl_loader_gltf_sheen.ts @@ -0,0 +1,72 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, controls; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 20); + camera.position.set(-0.75, 0.7, 1.25); + + scene = new THREE.Scene(); + + // model + + new GLTFLoader().setPath('models/gltf/').load('SheenChair.glb', function (gltf) { + scene.add(gltf.scene); + + const object = gltf.scene.getObjectByName('SheenChair_fabric'); + + const gui = new GUI(); + + gui.add(object.material, 'sheen', 0, 1); + gui.open(); + }); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1; + container.appendChild(renderer.domElement); + + const environment = new RoomEnvironment(); + const pmremGenerator = new THREE.PMREMGenerator(renderer); + + scene.background = new THREE.Color(0xbbbbbb); + scene.environment = pmremGenerator.fromScene(environment).texture; + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.minDistance = 1; + controls.maxDistance = 10; + controls.target.set(0, 0.35, 0); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + controls.update(); // required if damping enabled + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_gltf_transmission.ts b/examples-testing/examples/webgl_loader_gltf_transmission.ts new file mode 100644 index 000000000..87a47d2c0 --- /dev/null +++ b/examples-testing/examples/webgl_loader_gltf_transmission.ts @@ -0,0 +1,80 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; + +let camera, scene, renderer, controls, clock, mixer; + +init(); + +function init() { + clock = new THREE.Clock(); + + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 20); + camera.position.set(0, 0.4, 0.7); + + scene = new THREE.Scene(); + + new RGBELoader().setPath('textures/equirectangular/').load('royal_esplanade_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + scene.backgroundBlurriness = 0.35; + + scene.environment = texture; + + // model + + new GLTFLoader() + .setPath('models/gltf/') + .setDRACOLoader(new DRACOLoader().setDecoderPath('jsm/libs/draco/gltf/')) + .load('IridescentDishWithOlives.glb', function (gltf) { + mixer = new THREE.AnimationMixer(gltf.scene); + mixer.clipAction(gltf.animations[0]).play(); + + scene.add(gltf.scene); + }); + }); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1; + container.appendChild(renderer.domElement); + + controls = new OrbitControls(camera, renderer.domElement); + controls.autoRotate = true; + controls.autoRotateSpeed = -0.75; + controls.enableDamping = true; + controls.minDistance = 0.5; + controls.maxDistance = 1; + controls.target.set(0, 0.1, 0); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + if (mixer) mixer.update(clock.getDelta()); + + controls.update(); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_imagebitmap.ts b/examples-testing/examples/webgl_loader_imagebitmap.ts new file mode 100644 index 000000000..1049e9857 --- /dev/null +++ b/examples-testing/examples/webgl_loader_imagebitmap.ts @@ -0,0 +1,109 @@ +import * as THREE from 'three'; + +let camera, scene, renderer; +let group, cubes; + +init(); + +function addImageBitmap() { + new THREE.ImageBitmapLoader().load( + 'textures/planets/earth_atmos_2048.jpg?' + performance.now(), + function (imageBitmap) { + const texture = new THREE.CanvasTexture(imageBitmap); + texture.colorSpace = THREE.SRGBColorSpace; + const material = new THREE.MeshBasicMaterial({ map: texture }); + + /* ImageBitmap should be disposed when done with it + Can't be done until it's actually uploaded to WebGLTexture */ + + // imageBitmap.close(); + + addCube(material); + }, + function (p) { + console.log(p); + }, + function (e) { + console.log(e); + }, + ); +} + +function addImage() { + new THREE.ImageLoader() + .setCrossOrigin('*') + .load('textures/planets/earth_atmos_2048.jpg?' + performance.now(), function (image) { + const texture = new THREE.CanvasTexture(image); + texture.colorSpace = THREE.SRGBColorSpace; + const material = new THREE.MeshBasicMaterial({ color: 0xff8888, map: texture }); + addCube(material); + }); +} + +const geometry = new THREE.BoxGeometry(); + +function addCube(material) { + const cube = new THREE.Mesh(geometry, material); + cube.position.set(Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1); + cube.rotation.set(Math.random() * 2 * Math.PI, Math.random() * 2 * Math.PI, Math.random() * 2 * Math.PI); + cubes.add(cube); +} + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + // CAMERA + + camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 1, 1500); + camera.position.set(0, 4, 7); + camera.lookAt(0, 0, 0); + + // SCENE + + scene = new THREE.Scene(); + + // + + group = new THREE.Group(); + scene.add(group); + + group.add(new THREE.GridHelper(4, 12, 0x888888, 0x444444)); + + cubes = new THREE.Group(); + group.add(cubes); + + // RENDERER + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // TESTS + + setTimeout(addImage, 300); + setTimeout(addImage, 600); + setTimeout(addImage, 900); + setTimeout(addImageBitmap, 1300); + setTimeout(addImageBitmap, 1600); + setTimeout(addImageBitmap, 1900); + + // EVENTS + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + group.rotation.y = performance.now() / 3000; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_kmz.ts b/examples-testing/examples/webgl_loader_kmz.ts new file mode 100644 index 000000000..f93555e41 --- /dev/null +++ b/examples-testing/examples/webgl_loader_kmz.ts @@ -0,0 +1,59 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { KMZLoader } from 'three/addons/loaders/KMZLoader.js'; + +let camera, scene, renderer; + +init(); + +function init() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x999999); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0.5, 1.0, 0.5).normalize(); + + scene.add(light); + + camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 500); + + camera.position.y = 5; + camera.position.z = 10; + + scene.add(camera); + + const grid = new THREE.GridHelper(50, 50, 0xffffff, 0x7b7b7b); + scene.add(grid); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + const loader = new KMZLoader(); + loader.load('./models/kmz/Box.kmz', function (kmz) { + kmz.scene.position.y = 0.5; + scene.add(kmz.scene); + render(); + }); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_lwo.ts b/examples-testing/examples/webgl_loader_lwo.ts new file mode 100644 index 000000000..fb10c8340 --- /dev/null +++ b/examples-testing/examples/webgl_loader_lwo.ts @@ -0,0 +1,69 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { LWOLoader } from 'three/addons/loaders/LWOLoader.js'; + +let camera, scene, renderer; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 200); + camera.position.set(0.7, 14.6, -43.2); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xa0a0a0); + + const ambientLight = new THREE.AmbientLight(0xbbbbbb); + scene.add(ambientLight); + + const light1 = new THREE.DirectionalLight(0xc1c1c1, 3); + light1.position.set(0, 200, -100); + scene.add(light1); + + const grid = new THREE.GridHelper(200, 20, 0x000000, 0x000000); + grid.material.opacity = 0.3; + grid.material.transparent = true; + scene.add(grid); + + const loader = new LWOLoader(); + loader.load('models/lwo/Objects/LWO3/Demo.lwo', function (object) { + const phong = object.meshes[0]; + phong.position.set(2, 12, 0); + + const standard = object.meshes[1]; + standard.position.set(-2, 12, 0); + + const rocket = object.meshes[2]; + rocket.position.set(0, 10.5, 1); + + scene.add(phong, standard, rocket); + }); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + container.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(-1.33, 10, 6.7); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_md2_control.ts b/examples-testing/examples/webgl_loader_md2_control.ts new file mode 100644 index 000000000..683e4c2ad --- /dev/null +++ b/examples-testing/examples/webgl_loader_md2_control.ts @@ -0,0 +1,289 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { MD2CharacterComplex } from 'three/addons/misc/MD2CharacterComplex.js'; +import { Gyroscope } from 'three/addons/misc/Gyroscope.js'; + +let SCREEN_WIDTH = window.innerWidth; +let SCREEN_HEIGHT = window.innerHeight; + +let container, stats; +let camera, scene, renderer; + +const characters = []; +let nCharacters = 0; + +let cameraControls; + +const controls = { + moveForward: false, + moveBackward: false, + moveLeft: false, + moveRight: false, +}; + +const clock = new THREE.Clock(); + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + // CAMERA + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 4000); + camera.position.set(0, 150, 1300); + + // SCENE + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + scene.fog = new THREE.Fog(0xffffff, 1000, 4000); + + scene.add(camera); + + // LIGHTS + + scene.add(new THREE.AmbientLight(0x666666, 3)); + + const light = new THREE.DirectionalLight(0xffffff, 7); + light.position.set(200, 450, 500); + + light.castShadow = true; + + light.shadow.mapSize.width = 1024; + light.shadow.mapSize.height = 512; + + light.shadow.camera.near = 100; + light.shadow.camera.far = 1200; + + light.shadow.camera.left = -1000; + light.shadow.camera.right = 1000; + light.shadow.camera.top = 350; + light.shadow.camera.bottom = -350; + + scene.add(light); + // scene.add( new THREE.CameraHelper( light.shadow.camera ) ); + + // GROUND + + const gt = new THREE.TextureLoader().load('textures/terrain/grasslight-big.jpg'); + const gg = new THREE.PlaneGeometry(16000, 16000); + const gm = new THREE.MeshPhongMaterial({ color: 0xffffff, map: gt }); + + const ground = new THREE.Mesh(gg, gm); + ground.rotation.x = -Math.PI / 2; + ground.material.map.repeat.set(64, 64); + ground.material.map.wrapS = THREE.RepeatWrapping; + ground.material.map.wrapT = THREE.RepeatWrapping; + ground.material.map.colorSpace = THREE.SRGBColorSpace; + // note that because the ground does not cast a shadow, .castShadow is left false + ground.receiveShadow = true; + + scene.add(ground); + + // RENDERER + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.PCFSoftShadowMap; + + // STATS + + stats = new Stats(); + container.appendChild(stats.dom); + + // EVENTS + + window.addEventListener('resize', onWindowResize); + document.addEventListener('keydown', onKeyDown); + document.addEventListener('keyup', onKeyUp); + + // CONTROLS + + cameraControls = new OrbitControls(camera, renderer.domElement); + cameraControls.target.set(0, 50, 0); + cameraControls.update(); + + // CHARACTER + + const configOgro = { + baseUrl: 'models/md2/ogro/', + + body: 'ogro.md2', + skins: [ + 'grok.jpg', + 'ogrobase.png', + 'arboshak.png', + 'ctf_r.png', + 'ctf_b.png', + 'darkam.png', + 'freedom.png', + 'gib.png', + 'gordogh.png', + 'igdosh.png', + 'khorne.png', + 'nabogro.png', + 'sharokh.png', + ], + weapons: [['weapon.md2', 'weapon.jpg']], + animations: { + move: 'run', + idle: 'stand', + jump: 'jump', + attack: 'attack', + crouchMove: 'cwalk', + crouchIdle: 'cstand', + crouchAttach: 'crattack', + }, + + walkSpeed: 350, + crouchSpeed: 175, + }; + + const nRows = 1; + const nSkins = configOgro.skins.length; + + nCharacters = nSkins * nRows; + + for (let i = 0; i < nCharacters; i++) { + const character = new MD2CharacterComplex(); + character.scale = 3; + character.controls = controls; + characters.push(character); + } + + const baseCharacter = new MD2CharacterComplex(); + baseCharacter.scale = 3; + + baseCharacter.onLoadComplete = function () { + let k = 0; + + for (let j = 0; j < nRows; j++) { + for (let i = 0; i < nSkins; i++) { + const cloneCharacter = characters[k]; + + cloneCharacter.shareParts(baseCharacter); + + // cast and receive shadows + cloneCharacter.enableShadows(true); + + cloneCharacter.setWeapon(0); + cloneCharacter.setSkin(i); + + cloneCharacter.root.position.x = (i - nSkins / 2) * 150; + cloneCharacter.root.position.z = j * 250; + + scene.add(cloneCharacter.root); + + k++; + } + } + + const gyro = new Gyroscope(); + gyro.add(camera); + gyro.add(light, light.target); + + characters[Math.floor(nSkins / 2)].root.add(gyro); + }; + + baseCharacter.loadParts(configOgro); +} + +// EVENT HANDLERS + +function onWindowResize() { + SCREEN_WIDTH = window.innerWidth; + SCREEN_HEIGHT = window.innerHeight; + + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + + camera.aspect = SCREEN_WIDTH / SCREEN_HEIGHT; + camera.updateProjectionMatrix(); +} + +function onKeyDown(event) { + switch (event.code) { + case 'ArrowUp': + case 'KeyW': + controls.moveForward = true; + break; + + case 'ArrowDown': + case 'KeyS': + controls.moveBackward = true; + break; + + case 'ArrowLeft': + case 'KeyA': + controls.moveLeft = true; + break; + + case 'ArrowRight': + case 'KeyD': + controls.moveRight = true; + break; + + // case 'KeyC': controls.crouch = true; break; + // case 'Space': controls.jump = true; break; + // case 'ControlLeft': + // case 'ControlRight': controls.attack = true; break; + } +} + +function onKeyUp(event) { + switch (event.code) { + case 'ArrowUp': + case 'KeyW': + controls.moveForward = false; + break; + + case 'ArrowDown': + case 'KeyS': + controls.moveBackward = false; + break; + + case 'ArrowLeft': + case 'KeyA': + controls.moveLeft = false; + break; + + case 'ArrowRight': + case 'KeyD': + controls.moveRight = false; + break; + + // case 'KeyC': controls.crouch = false; break; + // case 'Space': controls.jump = false; break; + // case 'ControlLeft': + // case 'ControlRight': controls.attack = false; break; + } +} + +// + +function animate() { + render(); + + stats.update(); +} + +function render() { + const delta = clock.getDelta(); + + for (let i = 0; i < nCharacters; i++) { + characters[i].update(delta); + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_mdd.ts b/examples-testing/examples/webgl_loader_mdd.ts new file mode 100644 index 000000000..5b13e8f4b --- /dev/null +++ b/examples-testing/examples/webgl_loader_mdd.ts @@ -0,0 +1,62 @@ +import * as THREE from 'three'; + +import { MDDLoader } from 'three/addons/loaders/MDDLoader.js'; + +let camera, scene, renderer, mixer, clock; + +init(); + +function init() { + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(8, 8, 8); + camera.lookAt(scene.position); + + clock = new THREE.Clock(); + + // + + const loader = new MDDLoader(); + loader.load('models/mdd/cube.mdd', function (result) { + const morphTargets = result.morphTargets; + const clip = result.clip; + // clip.optimize(); // optional + + const geometry = new THREE.BoxGeometry(); + geometry.morphAttributes.position = morphTargets; // apply morph targets + + const material = new THREE.MeshNormalMaterial(); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + mixer = new THREE.AnimationMixer(mesh); + mixer.clipAction(clip).play(); // use clip + }); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const delta = clock.getDelta(); + + if (mixer) mixer.update(delta); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_obj.ts b/examples-testing/examples/webgl_loader_obj.ts new file mode 100644 index 000000000..f61eeb758 --- /dev/null +++ b/examples-testing/examples/webgl_loader_obj.ts @@ -0,0 +1,98 @@ +import * as THREE from 'three'; + +import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer; + +let object; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 20); + camera.position.z = 2.5; + + // scene + + scene = new THREE.Scene(); + + const ambientLight = new THREE.AmbientLight(0xffffff); + scene.add(ambientLight); + + const pointLight = new THREE.PointLight(0xffffff, 15); + camera.add(pointLight); + scene.add(camera); + + // manager + + function loadModel() { + object.traverse(function (child) { + if (child.isMesh) child.material.map = texture; + }); + + object.position.y = -0.95; + object.scale.setScalar(0.01); + scene.add(object); + + render(); + } + + const manager = new THREE.LoadingManager(loadModel); + + // texture + + const textureLoader = new THREE.TextureLoader(manager); + const texture = textureLoader.load('textures/uv_grid_opengl.jpg', render); + texture.colorSpace = THREE.SRGBColorSpace; + + // model + + function onProgress(xhr) { + if (xhr.lengthComputable) { + const percentComplete = (xhr.loaded / xhr.total) * 100; + console.log('model ' + percentComplete.toFixed(2) + '% downloaded'); + } + } + + function onError() {} + + const loader = new OBJLoader(manager); + loader.load( + 'models/obj/male02/male02.obj', + function (obj) { + object = obj; + }, + onProgress, + onError, + ); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 2; + controls.maxDistance = 5; + controls.addEventListener('change', render); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_obj_mtl.ts b/examples-testing/examples/webgl_loader_obj_mtl.ts new file mode 100644 index 000000000..4308aee7b --- /dev/null +++ b/examples-testing/examples/webgl_loader_obj_mtl.ts @@ -0,0 +1,82 @@ +import * as THREE from 'three'; + +import { MTLLoader } from 'three/addons/loaders/MTLLoader.js'; +import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 20); + camera.position.z = 2.5; + + // scene + + scene = new THREE.Scene(); + + const ambientLight = new THREE.AmbientLight(0xffffff); + scene.add(ambientLight); + + const pointLight = new THREE.PointLight(0xffffff, 15); + camera.add(pointLight); + scene.add(camera); + + // model + + const onProgress = function (xhr) { + if (xhr.lengthComputable) { + const percentComplete = (xhr.loaded / xhr.total) * 100; + console.log(percentComplete.toFixed(2) + '% downloaded'); + } + }; + + new MTLLoader().setPath('models/obj/male02/').load('male02.mtl', function (materials) { + materials.preload(); + + new OBJLoader() + .setMaterials(materials) + .setPath('models/obj/male02/') + .load( + 'male02.obj', + function (object) { + object.position.y = -0.95; + object.scale.setScalar(0.01); + scene.add(object); + }, + onProgress, + ); + }); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 2; + controls.maxDistance = 5; + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_pcd.ts b/examples-testing/examples/webgl_loader_pcd.ts new file mode 100644 index 000000000..d69e3fa2a --- /dev/null +++ b/examples-testing/examples/webgl_loader_pcd.ts @@ -0,0 +1,65 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { PCDLoader } from 'three/addons/loaders/PCDLoader.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer; + +init(); +render(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 0.01, 40); + camera.position.set(0, 0, 1); + scene.add(camera); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use if there is no animation loop + controls.minDistance = 0.5; + controls.maxDistance = 10; + + //scene.add( new THREE.AxesHelper( 1 ) ); + + const loader = new PCDLoader(); + loader.load('./models/pcd/binary/Zaghetto.pcd', function (points) { + points.geometry.center(); + points.geometry.rotateX(Math.PI); + points.name = 'Zaghetto.pcd'; + scene.add(points); + + // + + const gui = new GUI(); + + gui.add(points.material, 'size', 0.001, 0.01).onChange(render); + gui.addColor(points.material, 'color').onChange(render); + gui.open(); + + // + + render(); + }); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_pdb.ts b/examples-testing/examples/webgl_loader_pdb.ts new file mode 100644 index 000000000..b560efa73 --- /dev/null +++ b/examples-testing/examples/webgl_loader_pdb.ts @@ -0,0 +1,208 @@ +import * as THREE from 'three'; + +import { TrackballControls } from 'three/addons/controls/TrackballControls.js'; +import { PDBLoader } from 'three/addons/loaders/PDBLoader.js'; +import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, labelRenderer; +let controls; + +let root; + +const MOLECULES = { + Ethanol: 'ethanol.pdb', + Aspirin: 'aspirin.pdb', + Caffeine: 'caffeine.pdb', + Nicotine: 'nicotine.pdb', + LSD: 'lsd.pdb', + Cocaine: 'cocaine.pdb', + Cholesterol: 'cholesterol.pdb', + Lycopene: 'lycopene.pdb', + Glucose: 'glucose.pdb', + 'Aluminium oxide': 'Al2O3.pdb', + Cubane: 'cubane.pdb', + Copper: 'cu.pdb', + Fluorite: 'caf2.pdb', + Salt: 'nacl.pdb', + 'YBCO superconductor': 'ybco.pdb', + Buckyball: 'buckyball.pdb', + Graphite: 'graphite.pdb', +}; + +const params = { + molecule: 'caffeine.pdb', +}; + +const loader = new PDBLoader(); +const offset = new THREE.Vector3(); + +init(); + +function init() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050505); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 5000); + camera.position.z = 1000; + scene.add(camera); + + const light1 = new THREE.DirectionalLight(0xffffff, 2.5); + light1.position.set(1, 1, 1); + scene.add(light1); + + const light2 = new THREE.DirectionalLight(0xffffff, 1.5); + light2.position.set(-1, -1, 1); + scene.add(light2); + + root = new THREE.Group(); + scene.add(root); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.getElementById('container').appendChild(renderer.domElement); + + labelRenderer = new CSS2DRenderer(); + labelRenderer.setSize(window.innerWidth, window.innerHeight); + labelRenderer.domElement.style.position = 'absolute'; + labelRenderer.domElement.style.top = '0px'; + labelRenderer.domElement.style.pointerEvents = 'none'; + document.getElementById('container').appendChild(labelRenderer.domElement); + + // + + controls = new TrackballControls(camera, renderer.domElement); + controls.minDistance = 500; + controls.maxDistance = 2000; + + // + + loadMolecule(params.molecule); + + // + + window.addEventListener('resize', onWindowResize); + + // + + const gui = new GUI(); + + gui.add(params, 'molecule', MOLECULES).onChange(loadMolecule); + gui.open(); +} + +// + +function loadMolecule(model) { + const url = 'models/pdb/' + model; + + while (root.children.length > 0) { + const object = root.children[0]; + object.parent.remove(object); + } + + loader.load(url, function (pdb) { + const geometryAtoms = pdb.geometryAtoms; + const geometryBonds = pdb.geometryBonds; + const json = pdb.json; + + const boxGeometry = new THREE.BoxGeometry(1, 1, 1); + const sphereGeometry = new THREE.IcosahedronGeometry(1, 3); + + geometryAtoms.computeBoundingBox(); + geometryAtoms.boundingBox.getCenter(offset).negate(); + + geometryAtoms.translate(offset.x, offset.y, offset.z); + geometryBonds.translate(offset.x, offset.y, offset.z); + + let positions = geometryAtoms.getAttribute('position'); + const colors = geometryAtoms.getAttribute('color'); + + const position = new THREE.Vector3(); + const color = new THREE.Color(); + + for (let i = 0; i < positions.count; i++) { + position.x = positions.getX(i); + position.y = positions.getY(i); + position.z = positions.getZ(i); + + color.r = colors.getX(i); + color.g = colors.getY(i); + color.b = colors.getZ(i); + + const material = new THREE.MeshPhongMaterial({ color: color }); + + const object = new THREE.Mesh(sphereGeometry, material); + object.position.copy(position); + object.position.multiplyScalar(75); + object.scale.multiplyScalar(25); + root.add(object); + + const atom = json.atoms[i]; + + const text = document.createElement('div'); + text.className = 'label'; + text.style.color = 'rgb(' + atom[3][0] + ',' + atom[3][1] + ',' + atom[3][2] + ')'; + text.textContent = atom[4]; + + const label = new CSS2DObject(text); + label.position.copy(object.position); + root.add(label); + } + + positions = geometryBonds.getAttribute('position'); + + const start = new THREE.Vector3(); + const end = new THREE.Vector3(); + + for (let i = 0; i < positions.count; i += 2) { + start.x = positions.getX(i); + start.y = positions.getY(i); + start.z = positions.getZ(i); + + end.x = positions.getX(i + 1); + end.y = positions.getY(i + 1); + end.z = positions.getZ(i + 1); + + start.multiplyScalar(75); + end.multiplyScalar(75); + + const object = new THREE.Mesh(boxGeometry, new THREE.MeshPhongMaterial({ color: 0xffffff })); + object.position.copy(start); + object.position.lerp(end, 0.5); + object.scale.set(5, 5, start.distanceTo(end)); + object.lookAt(end); + root.add(object); + } + }); +} + +// + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + labelRenderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(); + + const time = Date.now() * 0.0004; + + root.rotation.x = time; + root.rotation.y = time * 0.7; + + render(); +} + +function render() { + renderer.render(scene, camera); + labelRenderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_ply.ts b/examples-testing/examples/webgl_loader_ply.ts new file mode 100644 index 000000000..0f4042b7d --- /dev/null +++ b/examples-testing/examples/webgl_loader_ply.ts @@ -0,0 +1,146 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { PLYLoader } from 'three/addons/loaders/PLYLoader.js'; + +let container, stats; + +let camera, cameraTarget, scene, renderer; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 15); + camera.position.set(3, 0.15, 3); + + cameraTarget = new THREE.Vector3(0, -0.1, 0); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x72645b); + scene.fog = new THREE.Fog(0x72645b, 2, 15); + + // Ground + + const plane = new THREE.Mesh( + new THREE.PlaneGeometry(40, 40), + new THREE.MeshPhongMaterial({ color: 0xcbcbcb, specular: 0x474747 }), + ); + plane.rotation.x = -Math.PI / 2; + plane.position.y = -0.5; + scene.add(plane); + + plane.receiveShadow = true; + + // PLY file + + const loader = new PLYLoader(); + loader.load('./models/ply/ascii/dolphins.ply', function (geometry) { + geometry.computeVertexNormals(); + + const material = new THREE.MeshStandardMaterial({ color: 0x009cff, flatShading: true }); + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.y = -0.2; + mesh.position.z = 0.3; + mesh.rotation.x = -Math.PI / 2; + mesh.scale.multiplyScalar(0.001); + + mesh.castShadow = true; + mesh.receiveShadow = true; + + scene.add(mesh); + }); + + loader.load('./models/ply/binary/Lucy100k.ply', function (geometry) { + geometry.computeVertexNormals(); + + const material = new THREE.MeshStandardMaterial({ color: 0x009cff, flatShading: true }); + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.x = -0.2; + mesh.position.y = -0.02; + mesh.position.z = -0.2; + mesh.scale.multiplyScalar(0.0006); + + mesh.castShadow = true; + mesh.receiveShadow = true; + + scene.add(mesh); + }); + + // Lights + + scene.add(new THREE.HemisphereLight(0x8d7c7c, 0x494966, 3)); + + addShadowedLight(1, 1, 1, 0xffffff, 3.5); + addShadowedLight(0.5, 1, -1, 0xffd500, 3); + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + + renderer.shadowMap.enabled = true; + + container.appendChild(renderer.domElement); + + // stats + + stats = new Stats(); + container.appendChild(stats.dom); + + // resize + + window.addEventListener('resize', onWindowResize); +} + +function addShadowedLight(x, y, z, color, intensity) { + const directionalLight = new THREE.DirectionalLight(color, intensity); + directionalLight.position.set(x, y, z); + scene.add(directionalLight); + + directionalLight.castShadow = true; + + const d = 1; + directionalLight.shadow.camera.left = -d; + directionalLight.shadow.camera.right = d; + directionalLight.shadow.camera.top = d; + directionalLight.shadow.camera.bottom = -d; + + directionalLight.shadow.camera.near = 1; + directionalLight.shadow.camera.far = 4; + + directionalLight.shadow.mapSize.width = 1024; + directionalLight.shadow.mapSize.height = 1024; + + directionalLight.shadow.bias = -0.001; +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + const timer = Date.now() * 0.0005; + + camera.position.x = Math.sin(timer) * 2.5; + camera.position.z = Math.cos(timer) * 2.5; + + camera.lookAt(cameraTarget); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_svg.ts b/examples-testing/examples/webgl_loader_svg.ts new file mode 100644 index 000000000..45361b92f --- /dev/null +++ b/examples-testing/examples/webgl_loader_svg.ts @@ -0,0 +1,193 @@ +import * as THREE from 'three'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { SVGLoader } from 'three/addons/loaders/SVGLoader.js'; + +let renderer, scene, camera, gui, guiData; + +init(); + +// + +function init() { + const container = document.getElementById('container'); + + // + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 0, 200); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + container.appendChild(renderer.domElement); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); + controls.screenSpacePanning = true; + + // + + window.addEventListener('resize', onWindowResize); + + guiData = { + currentURL: 'models/svg/tiger.svg', + drawFillShapes: true, + drawStrokes: true, + fillShapesWireframe: false, + strokesWireframe: false, + }; + + loadSVG(guiData.currentURL); + + createGUI(); +} + +function createGUI() { + if (gui) gui.destroy(); + + gui = new GUI(); + + gui.add(guiData, 'currentURL', { + Tiger: 'models/svg/tiger.svg', + 'Joins and caps': 'models/svg/lineJoinsAndCaps.svg', + Hexagon: 'models/svg/hexagon.svg', + Energy: 'models/svg/energy.svg', + 'Test 1': 'models/svg/tests/1.svg', + 'Test 2': 'models/svg/tests/2.svg', + 'Test 3': 'models/svg/tests/3.svg', + 'Test 4': 'models/svg/tests/4.svg', + 'Test 5': 'models/svg/tests/5.svg', + 'Test 6': 'models/svg/tests/6.svg', + 'Test 7': 'models/svg/tests/7.svg', + 'Test 8': 'models/svg/tests/8.svg', + 'Test 9': 'models/svg/tests/9.svg', + Units: 'models/svg/tests/units.svg', + Ordering: 'models/svg/tests/ordering.svg', + Defs: 'models/svg/tests/testDefs/Svg-defs.svg', + Defs2: 'models/svg/tests/testDefs/Svg-defs2.svg', + Defs3: 'models/svg/tests/testDefs/Wave-defs.svg', + Defs4: 'models/svg/tests/testDefs/defs4.svg', + Defs5: 'models/svg/tests/testDefs/defs5.svg', + 'Style CSS inside defs': 'models/svg/style-css-inside-defs.svg', + 'Multiple CSS classes': 'models/svg/multiple-css-classes.svg', + 'Zero Radius': 'models/svg/zero-radius.svg', + 'Styles in svg tag': 'models/svg/tests/styles.svg', + 'Round join': 'models/svg/tests/roundJoinPrecisionIssue.svg', + 'Ellipse Transformations': 'models/svg/tests/ellipseTransform.svg', + singlePointTest: 'models/svg/singlePointTest.svg', + singlePointTest2: 'models/svg/singlePointTest2.svg', + singlePointTest3: 'models/svg/singlePointTest3.svg', + emptyPath: 'models/svg/emptyPath.svg', + }) + .name('SVG File') + .onChange(update); + + gui.add(guiData, 'drawStrokes').name('Draw strokes').onChange(update); + + gui.add(guiData, 'drawFillShapes').name('Draw fill shapes').onChange(update); + + gui.add(guiData, 'strokesWireframe').name('Wireframe strokes').onChange(update); + + gui.add(guiData, 'fillShapesWireframe').name('Wireframe fill shapes').onChange(update); + + function update() { + loadSVG(guiData.currentURL); + } +} + +function loadSVG(url) { + // + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xb0b0b0); + + // + + const helper = new THREE.GridHelper(160, 10, 0x8d8d8d, 0xc1c1c1); + helper.rotation.x = Math.PI / 2; + scene.add(helper); + + // + + const loader = new SVGLoader(); + + loader.load(url, function (data) { + const group = new THREE.Group(); + group.scale.multiplyScalar(0.25); + group.position.x = -70; + group.position.y = 70; + group.scale.y *= -1; + + let renderOrder = 0; + + for (const path of data.paths) { + const fillColor = path.userData.style.fill; + + if (guiData.drawFillShapes && fillColor !== undefined && fillColor !== 'none') { + const material = new THREE.MeshBasicMaterial({ + color: new THREE.Color().setStyle(fillColor), + opacity: path.userData.style.fillOpacity, + transparent: true, + side: THREE.DoubleSide, + depthWrite: false, + wireframe: guiData.fillShapesWireframe, + }); + + const shapes = SVGLoader.createShapes(path); + + for (const shape of shapes) { + const geometry = new THREE.ShapeGeometry(shape); + const mesh = new THREE.Mesh(geometry, material); + mesh.renderOrder = renderOrder++; + + group.add(mesh); + } + } + + const strokeColor = path.userData.style.stroke; + + if (guiData.drawStrokes && strokeColor !== undefined && strokeColor !== 'none') { + const material = new THREE.MeshBasicMaterial({ + color: new THREE.Color().setStyle(strokeColor), + opacity: path.userData.style.strokeOpacity, + transparent: true, + side: THREE.DoubleSide, + depthWrite: false, + wireframe: guiData.strokesWireframe, + }); + + for (const subPath of path.subPaths) { + const geometry = SVGLoader.pointsToStroke(subPath.getPoints(), path.userData.style); + + if (geometry) { + const mesh = new THREE.Mesh(geometry, material); + mesh.renderOrder = renderOrder++; + + group.add(mesh); + } + } + } + } + + scene.add(group); + + render(); + }); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_texture_dds.ts b/examples-testing/examples/webgl_loader_texture_dds.ts new file mode 100644 index 000000000..bc4bd0572 --- /dev/null +++ b/examples-testing/examples/webgl_loader_texture_dds.ts @@ -0,0 +1,207 @@ +import * as THREE from 'three'; + +import { DDSLoader } from 'three/addons/loaders/DDSLoader.js'; + +let camera, scene, renderer; +const meshes = []; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.z = 15; + + scene = new THREE.Scene(); + + const geometry = new THREE.BoxGeometry(2, 2, 2); + + /* + This is how compressed textures are supposed to be used: + + DXT1 - RGB - opaque textures + DXT3 - RGBA - transparent textures with sharp alpha transitions + DXT5 - RGBA - transparent textures with full alpha range + */ + + const loader = new DDSLoader(); + + const map1 = loader.load('textures/compressed/disturb_dxt1_nomip.dds'); + map1.minFilter = map1.magFilter = THREE.LinearFilter; + map1.anisotropy = 4; + map1.colorSpace = THREE.SRGBColorSpace; + + const map2 = loader.load('textures/compressed/disturb_dxt1_mip.dds'); + map2.anisotropy = 4; + map2.colorSpace = THREE.SRGBColorSpace; + + const map3 = loader.load('textures/compressed/hepatica_dxt3_mip.dds'); + map3.anisotropy = 4; + map3.colorSpace = THREE.SRGBColorSpace; + + const map4 = loader.load('textures/compressed/explosion_dxt5_mip.dds'); + map4.anisotropy = 4; + map4.colorSpace = THREE.SRGBColorSpace; + + const map5 = loader.load('textures/compressed/disturb_argb_nomip.dds'); + map5.minFilter = map5.magFilter = THREE.LinearFilter; + map5.anisotropy = 4; + map5.colorSpace = THREE.SRGBColorSpace; + + const map6 = loader.load('textures/compressed/disturb_argb_mip.dds'); + map6.anisotropy = 4; + map6.colorSpace = THREE.SRGBColorSpace; + + const map7 = loader.load('textures/compressed/disturb_dx10_bc6h_signed_nomip.dds'); + map7.anisotropy = 4; + + const map8 = loader.load('textures/compressed/disturb_dx10_bc6h_signed_mip.dds'); + map8.anisotropy = 4; + + const map9 = loader.load('textures/compressed/disturb_dx10_bc6h_unsigned_nomip.dds'); + map9.anisotropy = 4; + + const map10 = loader.load('textures/compressed/disturb_dx10_bc6h_unsigned_mip.dds'); + map10.anisotropy = 4; + + const cubemap1 = loader.load('textures/compressed/Mountains.dds', function (texture) { + texture.magFilter = THREE.LinearFilter; + texture.minFilter = THREE.LinearFilter; + texture.mapping = THREE.CubeReflectionMapping; + texture.colorSpace = THREE.SRGBColorSpace; + material1.needsUpdate = true; + }); + + const cubemap2 = loader.load('textures/compressed/Mountains_argb_mip.dds', function (texture) { + texture.magFilter = THREE.LinearFilter; + texture.minFilter = THREE.LinearFilter; + texture.mapping = THREE.CubeReflectionMapping; + texture.colorSpace = THREE.SRGBColorSpace; + material5.needsUpdate = true; + }); + + const cubemap3 = loader.load('textures/compressed/Mountains_argb_nomip.dds', function (texture) { + texture.magFilter = THREE.LinearFilter; + texture.minFilter = THREE.LinearFilter; + texture.mapping = THREE.CubeReflectionMapping; + texture.colorSpace = THREE.SRGBColorSpace; + material6.needsUpdate = true; + }); + + const material1 = new THREE.MeshBasicMaterial({ map: map1, envMap: cubemap1 }); + const material2 = new THREE.MeshBasicMaterial({ map: map2 }); + const material3 = new THREE.MeshBasicMaterial({ map: map3, alphaTest: 0.5, side: THREE.DoubleSide }); + const material4 = new THREE.MeshBasicMaterial({ + map: map4, + side: THREE.DoubleSide, + blending: THREE.AdditiveBlending, + depthTest: false, + transparent: true, + }); + const material5 = new THREE.MeshBasicMaterial({ envMap: cubemap2 }); + const material6 = new THREE.MeshBasicMaterial({ envMap: cubemap3 }); + const material7 = new THREE.MeshBasicMaterial({ map: map5 }); + const material8 = new THREE.MeshBasicMaterial({ map: map6 }); + const material9 = new THREE.MeshBasicMaterial({ map: map7 }); + const material10 = new THREE.MeshBasicMaterial({ map: map8 }); + const material11 = new THREE.MeshBasicMaterial({ map: map9 }); + const material12 = new THREE.MeshBasicMaterial({ map: map10 }); + + let mesh = new THREE.Mesh(new THREE.TorusGeometry(), material1); + mesh.position.x = -10; + mesh.position.y = -2; + scene.add(mesh); + meshes.push(mesh); + + mesh = new THREE.Mesh(geometry, material2); + mesh.position.x = -6; + mesh.position.y = -2; + scene.add(mesh); + meshes.push(mesh); + + mesh = new THREE.Mesh(geometry, material3); + mesh.position.x = -6; + mesh.position.y = 2; + scene.add(mesh); + meshes.push(mesh); + + mesh = new THREE.Mesh(geometry, material4); + mesh.position.x = -10; + mesh.position.y = 2; + scene.add(mesh); + meshes.push(mesh); + + mesh = new THREE.Mesh(geometry, material5); + mesh.position.x = -2; + mesh.position.y = 2; + scene.add(mesh); + meshes.push(mesh); + + mesh = new THREE.Mesh(geometry, material6); + mesh.position.x = -2; + mesh.position.y = -2; + scene.add(mesh); + meshes.push(mesh); + + mesh = new THREE.Mesh(geometry, material7); + mesh.position.x = 2; + mesh.position.y = -2; + scene.add(mesh); + meshes.push(mesh); + + mesh = new THREE.Mesh(geometry, material8); + mesh.position.x = 2; + mesh.position.y = 2; + scene.add(mesh); + meshes.push(mesh); + + mesh = new THREE.Mesh(geometry, material9); + mesh.position.x = 6; + mesh.position.y = -2; + scene.add(mesh); + meshes.push(mesh); + + mesh = new THREE.Mesh(geometry, material10); + mesh.position.x = 6; + mesh.position.y = 2; + scene.add(mesh); + meshes.push(mesh); + + mesh = new THREE.Mesh(geometry, material11); + mesh.position.x = 10; + mesh.position.y = -2; + scene.add(mesh); + meshes.push(mesh); + + mesh = new THREE.Mesh(geometry, material12); + mesh.position.x = 10; + mesh.position.y = 2; + scene.add(mesh); + meshes.push(mesh); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const time = Date.now() * 0.001; + + for (let i = 0; i < meshes.length; i++) { + const mesh = meshes[i]; + mesh.rotation.x = time; + mesh.rotation.y = time; + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_texture_ktx.ts b/examples-testing/examples/webgl_loader_texture_ktx.ts new file mode 100644 index 000000000..af66eb810 --- /dev/null +++ b/examples-testing/examples/webgl_loader_texture_ktx.ts @@ -0,0 +1,137 @@ +import * as THREE from 'three'; + +import { KTXLoader } from 'three/addons/loaders/KTXLoader.js'; + +/* + This is how compressed textures are supposed to be used: + + best for desktop: + BC1(DXT1) - opaque textures + BC3(DXT5) - transparent textures with full alpha range + + best for iOS: + PVR2, PVR4 - opaque textures or alpha + + best for Android: + ETC1 - opaque textures + ASTC_4x4, ASTC8x8 - transparent textures with full alpha range + */ + +let camera, scene, renderer; +const meshes = []; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + const formats = { + astc: renderer.extensions.has('WEBGL_compressed_texture_astc'), + etc1: renderer.extensions.has('WEBGL_compressed_texture_etc1'), + s3tc: renderer.extensions.has('WEBGL_compressed_texture_s3tc'), + pvrtc: renderer.extensions.has('WEBGL_compressed_texture_pvrtc'), + }; + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 2000); + camera.position.z = 1000; + + scene = new THREE.Scene(); + + const geometry = new THREE.BoxGeometry(200, 200, 200); + let material1, material2; + + // TODO: add cubemap support + const loader = new KTXLoader(); + + if (formats.pvrtc) { + material1 = new THREE.MeshBasicMaterial({ + map: loader.load('textures/compressed/disturb_PVR2bpp.ktx'), + }); + material1.map.colorSpace = THREE.SRGBColorSpace; + material2 = new THREE.MeshBasicMaterial({ + map: loader.load('textures/compressed/lensflare_PVR4bpp.ktx'), + depthTest: false, + transparent: true, + side: THREE.DoubleSide, + }); + material2.map.colorSpace = THREE.SRGBColorSpace; + + meshes.push(new THREE.Mesh(geometry, material1)); + meshes.push(new THREE.Mesh(geometry, material2)); + } + + if (formats.s3tc) { + material1 = new THREE.MeshBasicMaterial({ + map: loader.load('textures/compressed/disturb_BC1.ktx'), + }); + material1.map.colorSpace = THREE.SRGBColorSpace; + material2 = new THREE.MeshBasicMaterial({ + map: loader.load('textures/compressed/lensflare_BC3.ktx'), + depthTest: false, + transparent: true, + side: THREE.DoubleSide, + }); + material2.map.colorSpace = THREE.SRGBColorSpace; + + meshes.push(new THREE.Mesh(geometry, material1)); + meshes.push(new THREE.Mesh(geometry, material2)); + } + + if (formats.etc1) { + material1 = new THREE.MeshBasicMaterial({ + map: loader.load('textures/compressed/disturb_ETC1.ktx'), + }); + + meshes.push(new THREE.Mesh(geometry, material1)); + } + + if (formats.astc) { + material1 = new THREE.MeshBasicMaterial({ + map: loader.load('textures/compressed/disturb_ASTC4x4.ktx'), + }); + material1.map.colorSpace = THREE.SRGBColorSpace; + material2 = new THREE.MeshBasicMaterial({ + map: loader.load('textures/compressed/lensflare_ASTC8x8.ktx'), + depthTest: false, + transparent: true, + side: THREE.DoubleSide, + }); + material2.map.colorSpace = THREE.SRGBColorSpace; + + meshes.push(new THREE.Mesh(geometry, material1)); + meshes.push(new THREE.Mesh(geometry, material2)); + } + + let x = (-meshes.length / 2) * 150; + for (let i = 0; i < meshes.length; ++i, x += 300) { + const mesh = meshes[i]; + mesh.position.x = x; + mesh.position.y = 0; + scene.add(mesh); + } + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const time = Date.now() * 0.001; + + for (let i = 0; i < meshes.length; i++) { + const mesh = meshes[i]; + mesh.rotation.x = time; + mesh.rotation.y = time; + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_texture_logluv.ts b/examples-testing/examples/webgl_loader_texture_logluv.ts new file mode 100644 index 000000000..7f3fbd4a0 --- /dev/null +++ b/examples-testing/examples/webgl_loader_texture_logluv.ts @@ -0,0 +1,75 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { LogLuvLoader } from 'three/addons/loaders/LogLuvLoader.js'; + +const params = { + exposure: 2.0, +}; + +let renderer, scene, camera; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + renderer.toneMapping = THREE.ReinhardToneMapping; + renderer.toneMappingExposure = params.exposure; + + scene = new THREE.Scene(); + + const aspect = window.innerWidth / window.innerHeight; + + camera = new THREE.OrthographicCamera(-aspect, aspect, 1, -1, 0, 1); + + new LogLuvLoader().load('textures/memorial.tif', function (texture) { + const material = new THREE.MeshBasicMaterial({ map: texture }); + + const quad = new THREE.PlaneGeometry(1, 1.5); + + const mesh = new THREE.Mesh(quad, material); + + scene.add(mesh); + + render(); + }); + + // + + const gui = new GUI(); + + gui.add(params, 'exposure', 0, 4, 0.01).onChange(render); + gui.open(); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const aspect = window.innerWidth / window.innerHeight; + + const frustumHeight = camera.top - camera.bottom; + + camera.left = (-frustumHeight * aspect) / 2; + camera.right = (frustumHeight * aspect) / 2; + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +// + +function render() { + renderer.toneMappingExposure = params.exposure; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_texture_rgbm.ts b/examples-testing/examples/webgl_loader_texture_rgbm.ts new file mode 100644 index 000000000..a882cdbc5 --- /dev/null +++ b/examples-testing/examples/webgl_loader_texture_rgbm.ts @@ -0,0 +1,75 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { RGBMLoader } from 'three/addons/loaders/RGBMLoader.js'; + +const params = { + exposure: 2.0, +}; + +let renderer, scene, camera; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + renderer.toneMapping = THREE.ReinhardToneMapping; + renderer.toneMappingExposure = params.exposure; + + scene = new THREE.Scene(); + + const aspect = window.innerWidth / window.innerHeight; + + camera = new THREE.OrthographicCamera(-aspect, aspect, 1, -1, 0, 1); + + new RGBMLoader().setMaxRange(16).load('textures/memorial.png', function (texture) { + const material = new THREE.MeshBasicMaterial({ map: texture }); + + const quad = new THREE.PlaneGeometry(1, 1.5); + + const mesh = new THREE.Mesh(quad, material); + + scene.add(mesh); + + render(); + }); + + // + + const gui = new GUI(); + + gui.add(params, 'exposure', 0, 4, 0.01).onChange(render); + gui.open(); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const aspect = window.innerWidth / window.innerHeight; + + const frustumHeight = camera.top - camera.bottom; + + camera.left = (-frustumHeight * aspect) / 2; + camera.right = (frustumHeight * aspect) / 2; + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +// + +function render() { + renderer.toneMappingExposure = params.exposure; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_texture_tga.ts b/examples-testing/examples/webgl_loader_texture_tga.ts new file mode 100644 index 000000000..c4f65b79a --- /dev/null +++ b/examples-testing/examples/webgl_loader_texture_tga.ts @@ -0,0 +1,90 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { TGALoader } from 'three/addons/loaders/TGALoader.js'; + +let camera, scene, renderer, stats; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0, 1, 5); + + scene = new THREE.Scene(); + + // + + const loader = new TGALoader(); + const geometry = new THREE.BoxGeometry(); + + // add box 1 - grey8 texture + + const texture1 = loader.load('textures/crate_grey8.tga'); + texture1.colorSpace = THREE.SRGBColorSpace; + const material1 = new THREE.MeshPhongMaterial({ color: 0xffffff, map: texture1 }); + + const mesh1 = new THREE.Mesh(geometry, material1); + mesh1.position.x = -1; + + scene.add(mesh1); + + // add box 2 - tga texture + + const texture2 = loader.load('textures/crate_color8.tga'); + texture2.colorSpace = THREE.SRGBColorSpace; + const material2 = new THREE.MeshPhongMaterial({ color: 0xffffff, map: texture2 }); + + const mesh2 = new THREE.Mesh(geometry, material2); + mesh2.position.x = 1; + + scene.add(mesh2); + + // + + const ambientLight = new THREE.AmbientLight(0xffffff, 1.5); + scene.add(ambientLight); + + const light = new THREE.DirectionalLight(0xffffff, 2.5); + light.position.set(1, 1, 1); + scene.add(light); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableZoom = false; + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); + stats.update(); +} diff --git a/examples-testing/examples/webgl_loader_texture_tiff.ts b/examples-testing/examples/webgl_loader_texture_tiff.ts new file mode 100644 index 000000000..f097774aa --- /dev/null +++ b/examples-testing/examples/webgl_loader_texture_tiff.ts @@ -0,0 +1,87 @@ +import * as THREE from 'three'; + +import { TIFFLoader } from 'three/addons/loaders/TIFFLoader.js'; + +let renderer, scene, camera; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 10); + camera.position.set(0, 0, 4); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + const loader = new TIFFLoader(); + + const geometry = new THREE.PlaneGeometry(); + + // uncompressed + + loader.load('textures/tiff/crate_uncompressed.tif', function (texture) { + texture.colorSpace = THREE.SRGBColorSpace; + + const material = new THREE.MeshBasicMaterial({ map: texture }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.set(-1.5, 0, 0); + + scene.add(mesh); + + render(); + }); + + // LZW + + loader.load('textures/tiff/crate_lzw.tif', function (texture) { + texture.colorSpace = THREE.SRGBColorSpace; + + const material = new THREE.MeshBasicMaterial({ map: texture }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.set(0, 0, 0); + + scene.add(mesh); + + render(); + }); + + // JPEG + + loader.load('textures/tiff/crate_jpeg.tif', function (texture) { + texture.colorSpace = THREE.SRGBColorSpace; + + const material = new THREE.MeshBasicMaterial({ map: texture }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.set(1.5, 0, 0); + + scene.add(mesh); + + render(); + }); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +// + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_texture_ultrahdr.ts b/examples-testing/examples/webgl_loader_texture_ultrahdr.ts new file mode 100644 index 000000000..c8bce4bf9 --- /dev/null +++ b/examples-testing/examples/webgl_loader_texture_ultrahdr.ts @@ -0,0 +1,101 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { UltraHDRLoader } from 'three/addons/loaders/UltraHDRLoader.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +const params = { + autoRotate: true, + metalness: 1.0, + roughness: 0.0, + exposure: 1.0, + resolution: '2k', + type: 'HalfFloatType', +}; + +let renderer, scene, camera, controls, torusMesh, loader; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = params.exposure; + + renderer.setAnimationLoop(render); + + scene = new THREE.Scene(); + + torusMesh = new THREE.Mesh( + new THREE.TorusKnotGeometry(1, 0.4, 128, 128, 1, 3), + new THREE.MeshStandardMaterial({ roughness: params.roughness, metalness: params.metalness }), + ); + scene.add(torusMesh); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 500); + camera.position.set(0.0, 0.0, -6.0); + + controls = new OrbitControls(camera, renderer.domElement); + + loader = new UltraHDRLoader(); + loader.setDataType(THREE.FloatType); + + const loadEnvironment = function (resolution = '2k', type = 'HalfFloatType') { + loader.setDataType(THREE[type]); + + loader.load(`textures/equirectangular/spruit_sunrise_${resolution}.hdr.jpg`, function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + texture.needsUpdate = true; + + scene.background = texture; + scene.environment = texture; + }); + }; + + loadEnvironment(params.resolution, params.type); + + const gui = new GUI(); + + gui.add(params, 'autoRotate'); + gui.add(params, 'metalness', 0, 1, 0.01); + gui.add(params, 'roughness', 0, 1, 0.01); + gui.add(params, 'exposure', 0, 4, 0.01); + gui.add(params, 'resolution', ['2k', '4k']).onChange(value => { + loadEnvironment(value, params.type); + }); + gui.add(params, 'type', ['HalfFloatType', 'FloatType']).onChange(value => { + loadEnvironment(params.resolution, value); + }); + + gui.open(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function render() { + torusMesh.material.roughness = params.roughness; + torusMesh.material.metalness = params.metalness; + + if (params.autoRotate) { + torusMesh.rotation.y += 0.005; + } + + renderer.toneMappingExposure = params.exposure; + + controls.update(); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_tilt.ts b/examples-testing/examples/webgl_loader_tilt.ts new file mode 100644 index 000000000..2a583c2b0 --- /dev/null +++ b/examples-testing/examples/webgl_loader_tilt.ts @@ -0,0 +1,54 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { TiltLoader } from 'three/addons/loaders/TiltLoader.js'; + +let camera, scene, renderer; + +init(); + +function init() { + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 500); + + camera.position.y = 43; + camera.position.z = 100; + + scene.add(camera); + + const grid = new THREE.GridHelper(50, 50, 0xffffff, 0x555555); + scene.add(grid); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + const loader = new TiltLoader(); + loader.load('./models/tilt/BRUSH_DOME.tilt', function (object) { + // console.log( object.children.length ); + scene.add(object); + render(); + }); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); + controls.target.y = camera.position.y; + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_ttf.ts b/examples-testing/examples/webgl_loader_ttf.ts new file mode 100644 index 000000000..168371a14 --- /dev/null +++ b/examples-testing/examples/webgl_loader_ttf.ts @@ -0,0 +1,231 @@ +import * as THREE from 'three'; + +import { TTFLoader } from 'three/addons/loaders/TTFLoader.js'; +import { Font } from 'three/addons/loaders/FontLoader.js'; +import { TextGeometry } from 'three/addons/geometries/TextGeometry.js'; + +let container; +let camera, cameraTarget, scene, renderer; +let group, textMesh1, textMesh2, textGeo, material; +let firstLetter = true; + +let text = 'three.js'; +const depth = 20, + size = 70, + hover = 30, + curveSegments = 4, + bevelThickness = 2, + bevelSize = 1.5; + +let font = null; +const mirror = true; + +let targetRotation = 0; +let targetRotationOnPointerDown = 0; + +let pointerX = 0; +let pointerXOnPointerDown = 0; + +let windowHalfX = window.innerWidth / 2; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + // CAMERA + + camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 1, 1500); + camera.position.set(0, 400, 700); + + cameraTarget = new THREE.Vector3(0, 150, 0); + + // SCENE + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x000000); + scene.fog = new THREE.Fog(0x000000, 250, 1400); + + // LIGHTS + + const dirLight1 = new THREE.DirectionalLight(0xffffff, 0.4); + dirLight1.position.set(0, 0, 1).normalize(); + scene.add(dirLight1); + + const dirLight2 = new THREE.DirectionalLight(0xffffff, 2); + dirLight2.position.set(0, hover, 10).normalize(); + dirLight2.color.setHSL(Math.random(), 1, 0.5, THREE.SRGBColorSpace); + scene.add(dirLight2); + + material = new THREE.MeshPhongMaterial({ color: 0xffffff, flatShading: true }); + + group = new THREE.Group(); + group.position.y = 100; + + scene.add(group); + + const loader = new TTFLoader(); + + loader.load('fonts/ttf/kenpixel.ttf', function (json) { + font = new Font(json); + createText(); + }); + + const plane = new THREE.Mesh( + new THREE.PlaneGeometry(10000, 10000), + new THREE.MeshBasicMaterial({ color: 0xffffff, opacity: 0.5, transparent: true }), + ); + plane.position.y = 100; + plane.rotation.x = -Math.PI / 2; + scene.add(plane); + + // RENDERER + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // EVENTS + + container.style.touchAction = 'none'; + container.addEventListener('pointerdown', onPointerDown); + + document.addEventListener('keypress', onDocumentKeyPress); + document.addEventListener('keydown', onDocumentKeyDown); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onDocumentKeyDown(event) { + if (firstLetter) { + firstLetter = false; + text = ''; + } + + const keyCode = event.keyCode; + + // backspace + + if (keyCode === 8) { + event.preventDefault(); + + text = text.substring(0, text.length - 1); + refreshText(); + + return false; + } +} + +function onDocumentKeyPress(event) { + const keyCode = event.which; + + // backspace + + if (keyCode === 8) { + event.preventDefault(); + } else { + const ch = String.fromCharCode(keyCode); + text += ch; + + refreshText(); + } +} + +function createText() { + textGeo = new TextGeometry(text, { + font: font, + + size: size, + depth: depth, + curveSegments: curveSegments, + + bevelThickness: bevelThickness, + bevelSize: bevelSize, + bevelEnabled: true, + }); + + textGeo.computeBoundingBox(); + textGeo.computeVertexNormals(); + + const centerOffset = -0.5 * (textGeo.boundingBox.max.x - textGeo.boundingBox.min.x); + + textMesh1 = new THREE.Mesh(textGeo, material); + + textMesh1.position.x = centerOffset; + textMesh1.position.y = hover; + textMesh1.position.z = 0; + + textMesh1.rotation.x = 0; + textMesh1.rotation.y = Math.PI * 2; + + group.add(textMesh1); + + if (mirror) { + textMesh2 = new THREE.Mesh(textGeo, material); + + textMesh2.position.x = centerOffset; + textMesh2.position.y = -hover; + textMesh2.position.z = depth; + + textMesh2.rotation.x = Math.PI; + textMesh2.rotation.y = Math.PI * 2; + + group.add(textMesh2); + } +} + +function refreshText() { + group.remove(textMesh1); + if (mirror) group.remove(textMesh2); + + if (!text) return; + + createText(); +} + +function onPointerDown(event) { + if (event.isPrimary === false) return; + + pointerXOnPointerDown = event.clientX - windowHalfX; + targetRotationOnPointerDown = targetRotation; + + document.addEventListener('pointermove', onPointerMove); + document.addEventListener('pointerup', onPointerUp); +} + +function onPointerMove(event) { + if (event.isPrimary === false) return; + + pointerX = event.clientX - windowHalfX; + + targetRotation = targetRotationOnPointerDown + (pointerX - pointerXOnPointerDown) * 0.02; +} + +function onPointerUp() { + if (event.isPrimary === false) return; + + document.removeEventListener('pointermove', onPointerMove); + document.removeEventListener('pointerup', onPointerUp); +} + +// + +function animate() { + group.rotation.y += (targetRotation - group.rotation.y) * 0.05; + + camera.lookAt(cameraTarget); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_usdz.ts b/examples-testing/examples/webgl_loader_usdz.ts new file mode 100644 index 000000000..d75823d88 --- /dev/null +++ b/examples-testing/examples/webgl_loader_usdz.ts @@ -0,0 +1,68 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; +import { USDZLoader } from 'three/addons/loaders/USDZLoader.js'; + +let camera, scene, renderer; + +init(); + +async function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0, 0.75, -1.5); + + scene = new THREE.Scene(); + + const rgbeLoader = new RGBELoader().setPath('textures/equirectangular/'); + + const usdzLoader = new USDZLoader().setPath('models/usdz/'); + + const [texture, model] = await Promise.all([ + rgbeLoader.loadAsync('venice_sunset_1k.hdr'), + usdzLoader.loadAsync('saeukkang.usdz'), + ]); + + // environment + + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + scene.backgroundBlurriness = 0.5; + scene.environment = texture; + + // model + + model.position.y = 0.25; + model.position.z = -0.25; + scene.add(model); + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 2.0; + document.body.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 1; + controls.maxDistance = 8; + // controls.target.y = 15; + // controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_vox.ts b/examples-testing/examples/webgl_loader_vox.ts new file mode 100644 index 000000000..061848012 --- /dev/null +++ b/examples-testing/examples/webgl_loader_vox.ts @@ -0,0 +1,104 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { VOXLoader, VOXMesh } from 'three/addons/loaders/VOXLoader.js'; + +let camera, controls, scene, renderer; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.01, 10); + camera.position.set(0.175, 0.075, 0.175); + + scene = new THREE.Scene(); + scene.add(camera); + + // light + + const hemiLight = new THREE.HemisphereLight(0xcccccc, 0x444444, 3); + scene.add(hemiLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 2.5); + dirLight.position.set(1.5, 3, 2.5); + scene.add(dirLight); + + const dirLight2 = new THREE.DirectionalLight(0xffffff, 1.5); + dirLight2.position.set(-1.5, -3, -2.5); + scene.add(dirLight2); + + const loader = new VOXLoader(); + loader.load('models/vox/monu10.vox', function (chunks) { + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + + // displayPalette( chunk.palette ); + + const mesh = new VOXMesh(chunk); + mesh.scale.setScalar(0.0015); + scene.add(mesh); + } + }); + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // controls + + controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 0.1; + controls.maxDistance = 0.5; + + // + + window.addEventListener('resize', onWindowResize); +} + +/* + function displayPalette( palette ) { + + const canvas = document.createElement( 'canvas' ); + canvas.width = 8; + canvas.height = 32; + canvas.style.position = 'absolute'; + canvas.style.top = '0'; + canvas.style.width = '100px'; + canvas.style.imageRendering = 'pixelated'; + document.body.appendChild( canvas ); + + const context = canvas.getContext( '2d' ); + + for ( let c = 0; c < 256; c ++ ) { + + const x = c % 8; + const y = Math.floor( c / 8 ); + + const hex = palette[ c + 1 ]; + const r = hex >> 0 & 0xff; + const g = hex >> 8 & 0xff; + const b = hex >> 16 & 0xff; + context.fillStyle = `rgba(${r},${g},${b},1)`; + context.fillRect( x, 31 - y, 1, 1 ); + + } + + } + */ + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_loader_vrml.ts b/examples-testing/examples/webgl_loader_vrml.ts new file mode 100644 index 000000000..fecf4bb45 --- /dev/null +++ b/examples-testing/examples/webgl_loader_vrml.ts @@ -0,0 +1,118 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { VRMLLoader } from 'three/addons/loaders/VRMLLoader.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, stats, controls, loader; + +const params = { + asset: 'house', +}; + +const assets = [ + 'creaseAngle', + 'crystal', + 'house', + 'elevationGrid1', + 'elevationGrid2', + 'extrusion1', + 'extrusion2', + 'extrusion3', + 'lines', + 'linesTransparent', + 'meshWithLines', + 'meshWithTexture', + 'pixelTexture', + 'points', +]; + +let vrmlScene; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1e10); + camera.position.set(-10, 5, 10); + + scene = new THREE.Scene(); + scene.add(camera); + + // light + + const ambientLight = new THREE.AmbientLight(0xffffff, 1.2); + scene.add(ambientLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 2.0); + dirLight.position.set(200, 200, 200); + scene.add(dirLight); + + loader = new VRMLLoader(); + loadAsset(params.asset); + + // renderer + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // controls + + controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 1; + controls.maxDistance = 200; + controls.enableDamping = true; + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); + + // + + const gui = new GUI(); + gui.add(params, 'asset', assets).onChange(function (value) { + if (vrmlScene) { + vrmlScene.traverse(function (object) { + if (object.material) object.material.dispose(); + if (object.material && object.material.map) object.material.map.dispose(); + if (object.geometry) object.geometry.dispose(); + }); + + scene.remove(vrmlScene); + } + + loadAsset(value); + }); +} + +function loadAsset(asset) { + loader.load('models/vrml/' + asset + '.wrl', function (object) { + vrmlScene = object; + scene.add(object); + controls.reset(); + }); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(); // to support damping + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_loader_vtk.ts b/examples-testing/examples/webgl_loader_vtk.ts new file mode 100644 index 000000000..dfc798657 --- /dev/null +++ b/examples-testing/examples/webgl_loader_vtk.ts @@ -0,0 +1,123 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { TrackballControls } from 'three/addons/controls/TrackballControls.js'; +import { VTKLoader } from 'three/addons/loaders/VTKLoader.js'; + +let stats; + +let camera, controls, scene, renderer; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.01, 100); + camera.position.z = 0.2; + + scene = new THREE.Scene(); + + scene.add(camera); + + // light + + const hemiLight = new THREE.HemisphereLight(0xffffff, 0x000000, 3); + scene.add(hemiLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 1.5); + dirLight.position.set(2, 2, 2); + scene.add(dirLight); + + const loader = new VTKLoader(); + loader.load('models/vtk/bunny.vtk', function (geometry) { + geometry.center(); + geometry.computeVertexNormals(); + + const material = new THREE.MeshLambertMaterial({ color: 0xffffff }); + const mesh = new THREE.Mesh(geometry, material); + mesh.position.set(-0.075, 0.005, 0); + mesh.scale.multiplyScalar(0.2); + scene.add(mesh); + }); + + const loader1 = new VTKLoader(); + loader1.load('models/vtk/cube_ascii.vtp', function (geometry) { + geometry.computeVertexNormals(); + geometry.center(); + + const material = new THREE.MeshLambertMaterial({ color: 0x00ff00 }); + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.set(-0.025, 0, 0); + mesh.scale.multiplyScalar(0.01); + + scene.add(mesh); + }); + + const loader2 = new VTKLoader(); + loader2.load('models/vtk/cube_binary.vtp', function (geometry) { + geometry.computeVertexNormals(); + geometry.center(); + + const material = new THREE.MeshLambertMaterial({ color: 0x0000ff }); + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.set(0.025, 0, 0); + mesh.scale.multiplyScalar(0.01); + + scene.add(mesh); + }); + + const loader3 = new VTKLoader(); + loader3.load('models/vtk/cube_no_compression.vtp', function (geometry) { + geometry.computeVertexNormals(); + geometry.center(); + + const material = new THREE.MeshLambertMaterial({ color: 0xff0000 }); + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.set(0.075, 0, 0); + mesh.scale.multiplyScalar(0.01); + + scene.add(mesh); + }); + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // controls + + controls = new TrackballControls(camera, renderer.domElement); + controls.minDistance = 0.1; + controls.maxDistance = 0.5; + controls.rotateSpeed = 5.0; + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + controls.handleResize(); +} + +function animate() { + controls.update(); + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_loader_xyz.ts b/examples-testing/examples/webgl_loader_xyz.ts new file mode 100644 index 000000000..90e009840 --- /dev/null +++ b/examples-testing/examples/webgl_loader_xyz.ts @@ -0,0 +1,62 @@ +import * as THREE from 'three'; + +import { XYZLoader } from 'three/addons/loaders/XYZLoader.js'; + +let camera, scene, renderer, clock; + +let points; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(10, 7, 10); + + scene = new THREE.Scene(); + scene.add(camera); + camera.lookAt(scene.position); + + clock = new THREE.Clock(); + + const loader = new XYZLoader(); + loader.load('models/xyz/helix_201.xyz', function (geometry) { + geometry.center(); + + const vertexColors = geometry.hasAttribute('color') === true; + + const material = new THREE.PointsMaterial({ size: 0.1, vertexColors: vertexColors }); + + points = new THREE.Points(geometry, material); + scene.add(points); + }); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const delta = clock.getDelta(); + + if (points) { + points.rotation.x += delta * 0.2; + points.rotation.y += delta * 0.5; + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_lod.ts b/examples-testing/examples/webgl_lod.ts new file mode 100644 index 000000000..0bb9e7be0 --- /dev/null +++ b/examples-testing/examples/webgl_lod.ts @@ -0,0 +1,88 @@ +import * as THREE from 'three'; + +import { FlyControls } from 'three/addons/controls/FlyControls.js'; + +let container; + +let camera, scene, renderer, controls; + +const clock = new THREE.Clock(); + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 15000); + camera.position.z = 1000; + + scene = new THREE.Scene(); + scene.fog = new THREE.Fog(0x000000, 1, 15000); + + const pointLight = new THREE.PointLight(0xff2200, 3, 0, 0); + pointLight.position.set(0, 0, 0); + scene.add(pointLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.position.set(0, 0, 1).normalize(); + scene.add(dirLight); + + const geometry = [ + [new THREE.IcosahedronGeometry(100, 16), 50], + [new THREE.IcosahedronGeometry(100, 8), 300], + [new THREE.IcosahedronGeometry(100, 4), 1000], + [new THREE.IcosahedronGeometry(100, 2), 2000], + [new THREE.IcosahedronGeometry(100, 1), 8000], + ]; + + const material = new THREE.MeshLambertMaterial({ color: 0xffffff, wireframe: true }); + + for (let j = 0; j < 1000; j++) { + const lod = new THREE.LOD(); + + for (let i = 0; i < geometry.length; i++) { + const mesh = new THREE.Mesh(geometry[i][0], material); + mesh.scale.set(1.5, 1.5, 1.5); + mesh.updateMatrix(); + mesh.matrixAutoUpdate = false; + lod.addLevel(mesh, geometry[i][1]); + } + + lod.position.x = 10000 * (0.5 - Math.random()); + lod.position.y = 7500 * (0.5 - Math.random()); + lod.position.z = 10000 * (0.5 - Math.random()); + lod.updateMatrix(); + lod.matrixAutoUpdate = false; + scene.add(lod); + } + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + controls = new FlyControls(camera, renderer.domElement); + controls.movementSpeed = 1000; + controls.rollSpeed = Math.PI / 10; + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(clock.getDelta()); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_marchingcubes.ts b/examples-testing/examples/webgl_marchingcubes.ts new file mode 100644 index 000000000..d11df56a4 --- /dev/null +++ b/examples-testing/examples/webgl_marchingcubes.ts @@ -0,0 +1,311 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { MarchingCubes } from 'three/addons/objects/MarchingCubes.js'; +import { ToonShader1, ToonShader2, ToonShaderHatching, ToonShaderDotted } from 'three/addons/shaders/ToonShader.js'; + +let container, stats; + +let camera, scene, renderer; + +let materials, current_material; + +let light, pointLight, ambientLight; + +let effect, resolution; + +let effectController; + +let time = 0; + +const clock = new THREE.Clock(); + +init(); + +function init() { + container = document.getElementById('container'); + + // CAMERA + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.set(-500, 500, 1500); + + // SCENE + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050505); + + // LIGHTS + + light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0.5, 0.5, 1); + scene.add(light); + + pointLight = new THREE.PointLight(0xff7c00, 3, 0, 0); + pointLight.position.set(0, 0, 100); + scene.add(pointLight); + + ambientLight = new THREE.AmbientLight(0x323232, 3); + scene.add(ambientLight); + + // MATERIALS + + materials = generateMaterials(); + current_material = 'shiny'; + + // MARCHING CUBES + + resolution = 28; + + effect = new MarchingCubes(resolution, materials[current_material], true, true, 100000); + effect.position.set(0, 0, 0); + effect.scale.set(700, 700, 700); + + effect.enableUvs = false; + effect.enableColors = false; + + scene.add(effect); + + // RENDERER + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // CONTROLS + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 500; + controls.maxDistance = 5000; + + // STATS + + stats = new Stats(); + container.appendChild(stats.dom); + + // GUI + + setupGui(); + + // EVENTS + + window.addEventListener('resize', onWindowResize); +} + +// + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function generateMaterials() { + // environment map + + const path = 'textures/cube/SwedishRoyalCastle/'; + const format = '.jpg'; + const urls = [ + path + 'px' + format, + path + 'nx' + format, + path + 'py' + format, + path + 'ny' + format, + path + 'pz' + format, + path + 'nz' + format, + ]; + + const cubeTextureLoader = new THREE.CubeTextureLoader(); + + const reflectionCube = cubeTextureLoader.load(urls); + const refractionCube = cubeTextureLoader.load(urls); + refractionCube.mapping = THREE.CubeRefractionMapping; + + // toons + + const toonMaterial1 = createShaderMaterial(ToonShader1, light, ambientLight); + const toonMaterial2 = createShaderMaterial(ToonShader2, light, ambientLight); + const hatchingMaterial = createShaderMaterial(ToonShaderHatching, light, ambientLight); + const dottedMaterial = createShaderMaterial(ToonShaderDotted, light, ambientLight); + + const texture = new THREE.TextureLoader().load('textures/uv_grid_opengl.jpg'); + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + texture.colorSpace = THREE.SRGBColorSpace; + + const materials = { + shiny: new THREE.MeshStandardMaterial({ + color: 0x9c0000, + envMap: reflectionCube, + roughness: 0.1, + metalness: 1.0, + }), + chrome: new THREE.MeshLambertMaterial({ color: 0xffffff, envMap: reflectionCube }), + liquid: new THREE.MeshLambertMaterial({ color: 0xffffff, envMap: refractionCube, refractionRatio: 0.85 }), + matte: new THREE.MeshPhongMaterial({ specular: 0x494949, shininess: 1 }), + flat: new THREE.MeshLambertMaterial({ + /*TODO flatShading: true */ + }), + textured: new THREE.MeshPhongMaterial({ color: 0xffffff, specular: 0x111111, shininess: 1, map: texture }), + colors: new THREE.MeshPhongMaterial({ color: 0xffffff, specular: 0xffffff, shininess: 2, vertexColors: true }), + multiColors: new THREE.MeshPhongMaterial({ shininess: 2, vertexColors: true }), + plastic: new THREE.MeshPhongMaterial({ specular: 0xc1c1c1, shininess: 250 }), + toon1: toonMaterial1, + toon2: toonMaterial2, + hatching: hatchingMaterial, + dotted: dottedMaterial, + }; + + return materials; +} + +function createShaderMaterial(shader, light, ambientLight) { + const u = THREE.UniformsUtils.clone(shader.uniforms); + + const vs = shader.vertexShader; + const fs = shader.fragmentShader; + + const material = new THREE.ShaderMaterial({ uniforms: u, vertexShader: vs, fragmentShader: fs }); + + material.uniforms['uDirLightPos'].value = light.position; + material.uniforms['uDirLightColor'].value = light.color; + + material.uniforms['uAmbientLightColor'].value = ambientLight.color; + + return material; +} + +// + +function setupGui() { + const createHandler = function (id) { + return function () { + current_material = id; + + effect.material = materials[id]; + effect.enableUvs = current_material === 'textured' ? true : false; + effect.enableColors = current_material === 'colors' || current_material === 'multiColors' ? true : false; + }; + }; + + effectController = { + material: 'shiny', + + speed: 1.0, + numBlobs: 10, + resolution: 28, + isolation: 80, + + floor: true, + wallx: false, + wallz: false, + + dummy: function () {}, + }; + + let h; + + const gui = new GUI(); + + // material (type) + + h = gui.addFolder('Materials'); + + for (const m in materials) { + effectController[m] = createHandler(m); + h.add(effectController, m).name(m); + } + + // simulation + + h = gui.addFolder('Simulation'); + + h.add(effectController, 'speed', 0.1, 8.0, 0.05); + h.add(effectController, 'numBlobs', 1, 50, 1); + h.add(effectController, 'resolution', 14, 100, 1); + h.add(effectController, 'isolation', 10, 300, 1); + + h.add(effectController, 'floor'); + h.add(effectController, 'wallx'); + h.add(effectController, 'wallz'); +} + +// this controls content of marching cubes voxel field + +function updateCubes(object, time, numblobs, floor, wallx, wallz) { + object.reset(); + + // fill the field with some metaballs + + const rainbow = [ + new THREE.Color(0xff0000), + new THREE.Color(0xffbb00), + new THREE.Color(0xffff00), + new THREE.Color(0x00ff00), + new THREE.Color(0x0000ff), + new THREE.Color(0x9400bd), + new THREE.Color(0xc800eb), + ]; + const subtract = 12; + const strength = 1.2 / ((Math.sqrt(numblobs) - 1) / 4 + 1); + + for (let i = 0; i < numblobs; i++) { + const ballx = Math.sin(i + 1.26 * time * (1.03 + 0.5 * Math.cos(0.21 * i))) * 0.27 + 0.5; + const bally = Math.abs(Math.cos(i + 1.12 * time * Math.cos(1.22 + 0.1424 * i))) * 0.77; // dip into the floor + const ballz = Math.cos(i + 1.32 * time * 0.1 * Math.sin(0.92 + 0.53 * i)) * 0.27 + 0.5; + + if (current_material === 'multiColors') { + object.addBall(ballx, bally, ballz, strength, subtract, rainbow[i % 7]); + } else { + object.addBall(ballx, bally, ballz, strength, subtract); + } + } + + if (floor) object.addPlaneY(2, 12); + if (wallz) object.addPlaneZ(2, 12); + if (wallx) object.addPlaneX(2, 12); + + object.update(); +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + const delta = clock.getDelta(); + + time += delta * effectController.speed * 0.5; + + // marching cubes + + if (effectController.resolution !== resolution) { + resolution = effectController.resolution; + effect.init(Math.floor(resolution)); + } + + if (effectController.isolation !== effect.isolation) { + effect.isolation = effectController.isolation; + } + + updateCubes( + effect, + time, + effectController.numBlobs, + effectController.floor, + effectController.wallx, + effectController.wallz, + ); + + // render + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_alphahash.ts b/examples-testing/examples/webgl_materials_alphahash.ts new file mode 100644 index 000000000..1ecf95f26 --- /dev/null +++ b/examples-testing/examples/webgl_materials_alphahash.ts @@ -0,0 +1,178 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { TAARenderPass } from 'three/addons/postprocessing/TAARenderPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +let camera, scene, renderer, controls, stats, mesh, material; + +let composer, renderPass, taaRenderPass, outputPass; + +let needsUpdate = false; + +const amount = parseInt(window.location.search.slice(1)) || 3; +const count = Math.pow(amount, 3); + +const color = new THREE.Color(); + +const params = { + alpha: 0.5, + alphaHash: true, + taa: true, + sampleLevel: 2, +}; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(amount, amount, amount); + camera.lookAt(0, 0, 0); + + scene = new THREE.Scene(); + + const geometry = new THREE.IcosahedronGeometry(0.5, 3); + + material = new THREE.MeshStandardMaterial({ + color: 0xffffff, + alphaHash: params.alphaHash, + opacity: params.alpha, + }); + + mesh = new THREE.InstancedMesh(geometry, material, count); + + let i = 0; + const offset = (amount - 1) / 2; + + const matrix = new THREE.Matrix4(); + + for (let x = 0; x < amount; x++) { + for (let y = 0; y < amount; y++) { + for (let z = 0; z < amount; z++) { + matrix.setPosition(offset - x, offset - y, offset - z); + + mesh.setMatrixAt(i, matrix); + mesh.setColorAt(i, color.setHex(Math.random() * 0xffffff)); + + i++; + } + } + } + + scene.add(mesh); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + const environment = new RoomEnvironment(); + const pmremGenerator = new THREE.PMREMGenerator(renderer); + + scene.environment = pmremGenerator.fromScene(environment).texture; + environment.dispose(); + + // + + composer = new EffectComposer(renderer); + + renderPass = new RenderPass(scene, camera); + renderPass.enabled = false; + + taaRenderPass = new TAARenderPass(scene, camera); + + outputPass = new OutputPass(); + + composer.addPass(renderPass); + composer.addPass(taaRenderPass); + composer.addPass(outputPass); + + // + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableZoom = false; + controls.enablePan = false; + + controls.addEventListener('change', () => (needsUpdate = true)); + + // + + const gui = new GUI(); + + gui.add(params, 'alpha', 0, 1).onChange(onMaterialUpdate); + gui.add(params, 'alphaHash').onChange(onMaterialUpdate); + + const taaFolder = gui.addFolder('Temporal Anti-Aliasing'); + + taaFolder + .add(params, 'taa') + .name('enabled') + .onChange(() => { + renderPass.enabled = !params.taa; + taaRenderPass.enabled = params.taa; + + sampleLevelCtrl.enable(params.taa); + + needsUpdate = true; + }); + + const sampleLevelCtrl = taaFolder.add(params, 'sampleLevel', 0, 6, 1).onChange(() => (needsUpdate = true)); + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); + + needsUpdate = true; +} + +function onMaterialUpdate() { + material.opacity = params.alpha; + material.alphaHash = params.alphaHash; + material.transparent = !params.alphaHash; + material.depthWrite = params.alphaHash; + + material.needsUpdate = true; + needsUpdate = true; +} + +function animate() { + render(); + + stats.update(); +} + +function render() { + if (needsUpdate) { + taaRenderPass.accumulate = false; + taaRenderPass.sampleLevel = 0; + + needsUpdate = false; + } else { + taaRenderPass.accumulate = true; + taaRenderPass.sampleLevel = params.sampleLevel; + } + + composer.render(); +} diff --git a/examples-testing/examples/webgl_materials_blending.ts b/examples-testing/examples/webgl_materials_blending.ts new file mode 100644 index 000000000..11cc009bc --- /dev/null +++ b/examples-testing/examples/webgl_materials_blending.ts @@ -0,0 +1,147 @@ +import * as THREE from 'three'; + +let camera, scene, renderer; +let mapBg; + +const textureLoader = new THREE.TextureLoader(); + +init(); + +function init() { + // CAMERA + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 600; + + // SCENE + + scene = new THREE.Scene(); + + // BACKGROUND + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = canvas.height = 128; + ctx.fillStyle = '#ddd'; + ctx.fillRect(0, 0, 128, 128); + ctx.fillStyle = '#555'; + ctx.fillRect(0, 0, 64, 64); + ctx.fillStyle = '#999'; + ctx.fillRect(32, 32, 32, 32); + ctx.fillStyle = '#555'; + ctx.fillRect(64, 64, 64, 64); + ctx.fillStyle = '#777'; + ctx.fillRect(96, 96, 32, 32); + + mapBg = new THREE.CanvasTexture(canvas); + mapBg.colorSpace = THREE.SRGBColorSpace; + mapBg.wrapS = mapBg.wrapT = THREE.RepeatWrapping; + mapBg.repeat.set(64, 32); + + scene.background = mapBg; + + // OBJECTS + + const blendings = [ + { name: 'No', constant: THREE.NoBlending }, + { name: 'Normal', constant: THREE.NormalBlending }, + { name: 'Additive', constant: THREE.AdditiveBlending }, + { name: 'Subtractive', constant: THREE.SubtractiveBlending }, + { name: 'Multiply', constant: THREE.MultiplyBlending }, + ]; + + const assignSRGB = texture => { + texture.colorSpace = THREE.SRGBColorSpace; + }; + + const map0 = textureLoader.load('textures/uv_grid_opengl.jpg', assignSRGB); + const map1 = textureLoader.load('textures/sprite0.jpg', assignSRGB); + const map2 = textureLoader.load('textures/sprite0.png', assignSRGB); + const map3 = textureLoader.load('textures/lensflare/lensflare0.png', assignSRGB); + const map4 = textureLoader.load('textures/lensflare/lensflare0_alpha.png', assignSRGB); + + const geo1 = new THREE.PlaneGeometry(100, 100); + const geo2 = new THREE.PlaneGeometry(100, 25); + + addImageRow(map0, 300); + addImageRow(map1, 150); + addImageRow(map2, 0); + addImageRow(map3, -150); + addImageRow(map4, -300); + + function addImageRow(map, y) { + for (let i = 0; i < blendings.length; i++) { + const blending = blendings[i]; + + const material = new THREE.MeshBasicMaterial({ map: map }); + material.transparent = true; + material.blending = blending.constant; + + const x = (i - blendings.length / 2) * 110; + const z = 0; + + let mesh = new THREE.Mesh(geo1, material); + mesh.position.set(x, y, z); + scene.add(mesh); + + mesh = new THREE.Mesh(geo2, generateLabelMaterial(blending.name)); + mesh.position.set(x, y - 75, z); + scene.add(mesh); + } + } + + // RENDERER + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // EVENTS + + window.addEventListener('resize', onWindowResize); +} + +// + +function onWindowResize() { + const SCREEN_WIDTH = window.innerWidth; + const SCREEN_HEIGHT = window.innerHeight; + + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + + camera.aspect = SCREEN_WIDTH / SCREEN_HEIGHT; + camera.updateProjectionMatrix(); +} + +function generateLabelMaterial(text) { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = 128; + canvas.height = 32; + + ctx.fillStyle = 'rgba( 0, 0, 0, 0.95 )'; + ctx.fillRect(0, 0, 128, 32); + + ctx.fillStyle = 'white'; + ctx.font = 'bold 12pt arial'; + ctx.fillText(text, 10, 22); + + const map = new THREE.CanvasTexture(canvas); + map.colorSpace = THREE.SRGBColorSpace; + + const material = new THREE.MeshBasicMaterial({ map: map, transparent: true }); + + return material; +} + +function animate() { + const time = Date.now() * 0.00025; + const ox = (time * -0.01 * mapBg.repeat.x) % 1; + const oy = (time * -0.01 * mapBg.repeat.y) % 1; + + mapBg.offset.set(ox, oy); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_blending_custom.ts b/examples-testing/examples/webgl_materials_blending_custom.ts new file mode 100644 index 000000000..072447426 --- /dev/null +++ b/examples-testing/examples/webgl_materials_blending_custom.ts @@ -0,0 +1,214 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer; + +let mapBg; +const materials = []; + +const params = { + blendEquation: THREE.AddEquation, +}; + +const equations = { + Add: THREE.AddEquation, + Subtract: THREE.SubtractEquation, + ReverseSubtract: THREE.ReverseSubtractEquation, + Min: THREE.MinEquation, + Max: THREE.MaxEquation, +}; + +init(); + +function init() { + // CAMERA + + camera = new THREE.PerspectiveCamera(80, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 700; + + // SCENE + + scene = new THREE.Scene(); + + // BACKGROUND + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = canvas.height = 128; + ctx.fillStyle = '#ddd'; + ctx.fillRect(0, 0, 128, 128); + ctx.fillStyle = '#555'; + ctx.fillRect(0, 0, 64, 64); + ctx.fillStyle = '#999'; + ctx.fillRect(32, 32, 32, 32); + ctx.fillStyle = '#555'; + ctx.fillRect(64, 64, 64, 64); + ctx.fillStyle = '#777'; + ctx.fillRect(96, 96, 32, 32); + + mapBg = new THREE.CanvasTexture(canvas); + mapBg.colorSpace = THREE.SRGBColorSpace; + mapBg.wrapS = mapBg.wrapT = THREE.RepeatWrapping; + mapBg.repeat.set(64, 32); + + scene.background = mapBg; + + // FOREGROUND OBJECTS + + const src = [ + { name: 'Zero', constant: THREE.ZeroFactor }, + { name: 'One', constant: THREE.OneFactor }, + { name: 'SrcColor', constant: THREE.SrcColorFactor }, + { name: 'OneMinusSrcColor', constant: THREE.OneMinusSrcColorFactor }, + { name: 'SrcAlpha', constant: THREE.SrcAlphaFactor }, + { name: 'OneMinusSrcAlpha', constant: THREE.OneMinusSrcAlphaFactor }, + { name: 'DstAlpha', constant: THREE.DstAlphaFactor }, + { name: 'OneMinusDstAlpha', constant: THREE.OneMinusDstAlphaFactor }, + { name: 'DstColor', constant: THREE.DstColorFactor }, + { name: 'OneMinusDstColor', constant: THREE.OneMinusDstColorFactor }, + { name: 'SrcAlphaSaturate', constant: THREE.SrcAlphaSaturateFactor }, + ]; + + const dst = [ + { name: 'Zero', constant: THREE.ZeroFactor }, + { name: 'One', constant: THREE.OneFactor }, + { name: 'SrcColor', constant: THREE.SrcColorFactor }, + { name: 'OneMinusSrcColor', constant: THREE.OneMinusSrcColorFactor }, + { name: 'SrcAlpha', constant: THREE.SrcAlphaFactor }, + { name: 'OneMinusSrcAlpha', constant: THREE.OneMinusSrcAlphaFactor }, + { name: 'DstAlpha', constant: THREE.DstAlphaFactor }, + { name: 'OneMinusDstAlpha', constant: THREE.OneMinusDstAlphaFactor }, + { name: 'DstColor', constant: THREE.DstColorFactor }, + { name: 'OneMinusDstColor', constant: THREE.OneMinusDstColorFactor }, + ]; + + const geo1 = new THREE.PlaneGeometry(100, 100); + const geo2 = new THREE.PlaneGeometry(100, 25); + + const texture = new THREE.TextureLoader().load('textures/lensflare/lensflare0_alpha.png'); + texture.colorSpace = THREE.SRGBColorSpace; + + for (let i = 0; i < dst.length; i++) { + const blendDst = dst[i]; + + for (let j = 0; j < src.length; j++) { + const blendSrc = src[j]; + + const material = new THREE.MeshBasicMaterial({ map: texture }); + material.transparent = true; + + material.blending = THREE.CustomBlending; + material.blendSrc = blendSrc.constant; + material.blendDst = blendDst.constant; + material.blendEquation = THREE.AddEquation; + + const x = (j - src.length / 2) * 110; + const z = 0; + const y = (i - dst.length / 2) * 110 + 50; + + const mesh = new THREE.Mesh(geo1, material); + mesh.position.set(x, -y, z); + mesh.matrixAutoUpdate = false; + mesh.updateMatrix(); + scene.add(mesh); + + materials.push(material); + } + } + + for (let j = 0; j < src.length; j++) { + const blendSrc = src[j]; + + const x = (j - src.length / 2) * 110; + const z = 0; + const y = (0 - dst.length / 2) * 110 + 50; + + const mesh = new THREE.Mesh(geo2, generateLabelMaterial(blendSrc.name, 'rgba( 0, 150, 0, 1 )')); + mesh.position.set(x, -(y - 70), z); + mesh.matrixAutoUpdate = false; + mesh.updateMatrix(); + scene.add(mesh); + } + + for (let i = 0; i < dst.length; i++) { + const blendDst = dst[i]; + + const x = (0 - src.length / 2) * 110 - 125; + const z = 0; + const y = (i - dst.length / 2) * 110 + 165; + + const mesh = new THREE.Mesh(geo2, generateLabelMaterial(blendDst.name, 'rgba( 150, 0, 0, 1 )')); + mesh.position.set(x, -(y - 120), z); + mesh.matrixAutoUpdate = false; + mesh.updateMatrix(); + scene.add(mesh); + } + + // RENDERER + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // EVENTS + + window.addEventListener('resize', onWindowResize); + + // GUI + + // + const gui = new GUI({ width: 300 }); + + gui.add(params, 'blendEquation', equations).onChange(updateBlendEquation); + gui.open(); +} + +// + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); +} + +// + +function generateLabelMaterial(text, bg) { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = 128; + canvas.height = 32; + + ctx.fillStyle = bg; + ctx.fillRect(0, 0, 128, 32); + + ctx.fillStyle = 'white'; + ctx.font = 'bold 11pt arial'; + ctx.fillText(text, 8, 22); + + const map = new THREE.CanvasTexture(canvas); + map.colorSpace = THREE.SRGBColorSpace; + + const material = new THREE.MeshBasicMaterial({ map: map, transparent: true }); + return material; +} + +function updateBlendEquation(value) { + for (const material of materials) { + material.blendEquation = value; + } +} + +function animate() { + const time = Date.now() * 0.00025; + const ox = (time * -0.01 * mapBg.repeat.x) % 1; + const oy = (time * -0.01 * mapBg.repeat.y) % 1; + + mapBg.offset.set(ox, oy); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_bumpmap.ts b/examples-testing/examples/webgl_materials_bumpmap.ts new file mode 100644 index 000000000..d954fab7e --- /dev/null +++ b/examples-testing/examples/webgl_materials_bumpmap.ts @@ -0,0 +1,140 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +let container, stats, loader; + +let camera, scene, renderer; + +let mesh; + +let spotLight; + +let mouseX = 0; +let mouseY = 0; + +let targetX = 0; +let targetY = 0; + +const windowHalfX = window.innerWidth / 2; +const windowHalfY = window.innerHeight / 2; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + // + + camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.z = 12; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x060708); + + // LIGHTS + + scene.add(new THREE.HemisphereLight(0x8d7c7c, 0x494966, 3)); + + spotLight = new THREE.SpotLight(0xffffde, 200); + spotLight.position.set(3.5, 0, 7); + scene.add(spotLight); + + spotLight.castShadow = true; + + spotLight.shadow.mapSize.width = 2048; + spotLight.shadow.mapSize.height = 2048; + + spotLight.shadow.camera.near = 2; + spotLight.shadow.camera.far = 15; + + spotLight.shadow.camera.fov = 40; + + spotLight.shadow.bias = -0.005; + + // + + const mapHeight = new THREE.TextureLoader().load( + 'models/gltf/LeePerrySmith/Infinite-Level_02_Disp_NoSmoothUV-4096.jpg', + ); + + const material = new THREE.MeshPhongMaterial({ + color: 0x9c6e49, + specular: 0x666666, + shininess: 25, + bumpMap: mapHeight, + bumpScale: 10, + }); + + loader = new GLTFLoader(); + loader.load('models/gltf/LeePerrySmith/LeePerrySmith.glb', function (gltf) { + createScene(gltf.scene.children[0].geometry, 1, material); + }); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + renderer.shadowMap.enabled = true; + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // EVENTS + + document.addEventListener('mousemove', onDocumentMouseMove); + window.addEventListener('resize', onWindowResize); +} + +function createScene(geometry, scale, material) { + mesh = new THREE.Mesh(geometry, material); + + mesh.position.y = -0.5; + mesh.scale.set(scale, scale, scale); + + mesh.castShadow = true; + mesh.receiveShadow = true; + + scene.add(mesh); +} + +// + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); +} + +function onDocumentMouseMove(event) { + mouseX = event.clientX - windowHalfX; + mouseY = event.clientY - windowHalfY; +} + +// + +function animate() { + render(); + + stats.update(); +} + +function render() { + targetX = mouseX * 0.001; + targetY = mouseY * 0.001; + + if (mesh) { + mesh.rotation.y += 0.05 * (targetX - mesh.rotation.y); + mesh.rotation.x += 0.05 * (targetY - mesh.rotation.x); + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_car.ts b/examples-testing/examples/webgl_materials_car.ts new file mode 100644 index 000000000..e810f7b7d --- /dev/null +++ b/examples-testing/examples/webgl_materials_car.ts @@ -0,0 +1,167 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +let camera, scene, renderer; +let stats; + +let grid; +let controls; + +const wheels = []; + +function init() { + const container = document.getElementById('container'); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 0.85; + container.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(4.25, 1.4, -4.5); + + controls = new OrbitControls(camera, container); + controls.maxDistance = 9; + controls.maxPolarAngle = THREE.MathUtils.degToRad(90); + controls.target.set(0, 0.5, 0); + controls.update(); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x333333); + scene.environment = new RGBELoader().load('textures/equirectangular/venice_sunset_1k.hdr'); + scene.environment.mapping = THREE.EquirectangularReflectionMapping; + scene.fog = new THREE.Fog(0x333333, 10, 15); + + grid = new THREE.GridHelper(20, 40, 0xffffff, 0xffffff); + grid.material.opacity = 0.2; + grid.material.depthWrite = false; + grid.material.transparent = true; + scene.add(grid); + + // materials + + const bodyMaterial = new THREE.MeshPhysicalMaterial({ + color: 0xff0000, + metalness: 1.0, + roughness: 0.5, + clearcoat: 1.0, + clearcoatRoughness: 0.03, + }); + + const detailsMaterial = new THREE.MeshStandardMaterial({ + color: 0xffffff, + metalness: 1.0, + roughness: 0.5, + }); + + const glassMaterial = new THREE.MeshPhysicalMaterial({ + color: 0xffffff, + metalness: 0.25, + roughness: 0, + transmission: 1.0, + }); + + const bodyColorInput = document.getElementById('body-color'); + bodyColorInput.addEventListener('input', function () { + bodyMaterial.color.set(this.value); + }); + + const detailsColorInput = document.getElementById('details-color'); + detailsColorInput.addEventListener('input', function () { + detailsMaterial.color.set(this.value); + }); + + const glassColorInput = document.getElementById('glass-color'); + glassColorInput.addEventListener('input', function () { + glassMaterial.color.set(this.value); + }); + + // Car + + const shadow = new THREE.TextureLoader().load('models/gltf/ferrari_ao.png'); + + const dracoLoader = new DRACOLoader(); + dracoLoader.setDecoderPath('jsm/libs/draco/gltf/'); + + const loader = new GLTFLoader(); + loader.setDRACOLoader(dracoLoader); + + loader.load('models/gltf/ferrari.glb', function (gltf) { + const carModel = gltf.scene.children[0]; + + carModel.getObjectByName('body').material = bodyMaterial; + + carModel.getObjectByName('rim_fl').material = detailsMaterial; + carModel.getObjectByName('rim_fr').material = detailsMaterial; + carModel.getObjectByName('rim_rr').material = detailsMaterial; + carModel.getObjectByName('rim_rl').material = detailsMaterial; + carModel.getObjectByName('trim').material = detailsMaterial; + + carModel.getObjectByName('glass').material = glassMaterial; + + wheels.push( + carModel.getObjectByName('wheel_fl'), + carModel.getObjectByName('wheel_fr'), + carModel.getObjectByName('wheel_rl'), + carModel.getObjectByName('wheel_rr'), + ); + + // shadow + const mesh = new THREE.Mesh( + new THREE.PlaneGeometry(0.655 * 4, 1.3 * 4), + new THREE.MeshBasicMaterial({ + map: shadow, + blending: THREE.MultiplyBlending, + toneMapped: false, + transparent: true, + }), + ); + mesh.rotation.x = -Math.PI / 2; + mesh.renderOrder = 2; + carModel.add(mesh); + + scene.add(carModel); + }); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(); + + const time = -performance.now() / 1000; + + for (let i = 0; i < wheels.length; i++) { + wheels[i].rotation.x = time * Math.PI * 2; + } + + grid.position.z = -time % 1; + + renderer.render(scene, camera); + + stats.update(); +} + +init(); diff --git a/examples-testing/examples/webgl_materials_cubemap.ts b/examples-testing/examples/webgl_materials_cubemap.ts new file mode 100644 index 000000000..5f2692751 --- /dev/null +++ b/examples-testing/examples/webgl_materials_cubemap.ts @@ -0,0 +1,115 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'; + +let container, stats; + +let camera, scene, renderer; + +let pointLight; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.z = 13; + + //cubemap + const path = 'textures/cube/SwedishRoyalCastle/'; + const format = '.jpg'; + const urls = [ + path + 'px' + format, + path + 'nx' + format, + path + 'py' + format, + path + 'ny' + format, + path + 'pz' + format, + path + 'nz' + format, + ]; + + const reflectionCube = new THREE.CubeTextureLoader().load(urls); + const refractionCube = new THREE.CubeTextureLoader().load(urls); + refractionCube.mapping = THREE.CubeRefractionMapping; + + scene = new THREE.Scene(); + scene.background = reflectionCube; + + //lights + const ambient = new THREE.AmbientLight(0xffffff, 3); + scene.add(ambient); + + pointLight = new THREE.PointLight(0xffffff, 200); + scene.add(pointLight); + + //materials + const cubeMaterial3 = new THREE.MeshLambertMaterial({ + color: 0xffaa00, + envMap: reflectionCube, + combine: THREE.MixOperation, + reflectivity: 0.3, + }); + const cubeMaterial2 = new THREE.MeshLambertMaterial({ + color: 0xfff700, + envMap: refractionCube, + refractionRatio: 0.95, + }); + const cubeMaterial1 = new THREE.MeshLambertMaterial({ color: 0xffffff, envMap: reflectionCube }); + + //models + const objLoader = new OBJLoader(); + + objLoader.setPath('models/obj/walt/'); + objLoader.load('WaltHead.obj', function (object) { + const head = object.children[0]; + head.scale.setScalar(0.1); + head.position.y = -3; + head.material = cubeMaterial1; + + const head2 = head.clone(); + head2.position.x = -6; + head2.material = cubeMaterial2; + + const head3 = head.clone(); + head3.position.x = 6; + head3.material = cubeMaterial3; + + scene.add(head, head2, head3); + }); + + //renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + //controls + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableZoom = false; + controls.enablePan = false; + controls.minPolarAngle = Math.PI / 4; + controls.maxPolarAngle = Math.PI / 1.5; + + //stats + stats = new Stats(); + container.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); + stats.update(); +} diff --git a/examples-testing/examples/webgl_materials_cubemap_dynamic.ts b/examples-testing/examples/webgl_materials_cubemap_dynamic.ts new file mode 100644 index 000000000..13a268901 --- /dev/null +++ b/examples-testing/examples/webgl_materials_cubemap_dynamic.ts @@ -0,0 +1,115 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import Stats from 'three/addons/libs/stats.module.js'; + +let camera, scene, renderer, stats; +let cube, sphere, torus, material; + +let cubeCamera, cubeRenderTarget; + +let controls; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + document.body.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResized); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 75; + + scene = new THREE.Scene(); + scene.rotation.y = 0.5; // avoid flying objects occluding the sun + + new RGBELoader().setPath('textures/equirectangular/').load('quarry_01_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + scene.environment = texture; + }); + + // + + cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256); + cubeRenderTarget.texture.type = THREE.HalfFloatType; + + cubeCamera = new THREE.CubeCamera(1, 1000, cubeRenderTarget); + + // + + material = new THREE.MeshStandardMaterial({ + envMap: cubeRenderTarget.texture, + roughness: 0.05, + metalness: 1, + }); + + const gui = new GUI(); + gui.add(material, 'roughness', 0, 1); + gui.add(material, 'metalness', 0, 1); + gui.add(renderer, 'toneMappingExposure', 0, 2).name('exposure'); + + sphere = new THREE.Mesh(new THREE.IcosahedronGeometry(15, 8), material); + scene.add(sphere); + + const material2 = new THREE.MeshStandardMaterial({ + roughness: 0.1, + metalness: 0, + }); + + cube = new THREE.Mesh(new THREE.BoxGeometry(15, 15, 15), material2); + scene.add(cube); + + torus = new THREE.Mesh(new THREE.TorusKnotGeometry(8, 3, 128, 16), material2); + scene.add(torus); + + // + + controls = new OrbitControls(camera, renderer.domElement); + controls.autoRotate = true; +} + +function onWindowResized() { + renderer.setSize(window.innerWidth, window.innerHeight); + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); +} + +function animate(msTime) { + const time = msTime / 1000; + + cube.position.x = Math.cos(time) * 30; + cube.position.y = Math.sin(time) * 30; + cube.position.z = Math.sin(time) * 30; + + cube.rotation.x += 0.02; + cube.rotation.y += 0.03; + + torus.position.x = Math.cos(time + 10) * 30; + torus.position.y = Math.sin(time + 10) * 30; + torus.position.z = Math.sin(time + 10) * 30; + + torus.rotation.x += 0.02; + torus.rotation.y += 0.03; + + cubeCamera.update(renderer, scene); + + controls.update(); + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_materials_cubemap_mipmaps.ts b/examples-testing/examples/webgl_materials_cubemap_mipmaps.ts new file mode 100644 index 000000000..944f4c18e --- /dev/null +++ b/examples-testing/examples/webgl_materials_cubemap_mipmaps.ts @@ -0,0 +1,119 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let container; + +let camera, scene, renderer; + +init(); + +//load customized cube texture +async function loadCubeTextureWithMipmaps() { + const path = 'textures/cube/angus/'; + const format = '.jpg'; + const mipmaps = []; + const maxLevel = 8; + + async function loadCubeTexture(urls) { + return new Promise(function (resolve) { + new THREE.CubeTextureLoader().load(urls, function (cubeTexture) { + resolve(cubeTexture); + }); + }); + } + + // load mipmaps + const pendings = []; + + for (let level = 0; level <= maxLevel; ++level) { + const urls = []; + + for (let face = 0; face < 6; ++face) { + urls.push(path + 'cube_m0' + level + '_c0' + face + format); + } + + const mipmapLevel = level; + + pendings.push( + loadCubeTexture(urls).then(function (cubeTexture) { + mipmaps[mipmapLevel] = cubeTexture; + }), + ); + } + + await Promise.all(pendings); + + const customizedCubeTexture = mipmaps.shift(); + customizedCubeTexture.mipmaps = mipmaps; + customizedCubeTexture.colorSpace = THREE.SRGBColorSpace; + customizedCubeTexture.minFilter = THREE.LinearMipMapLinearFilter; + customizedCubeTexture.magFilter = THREE.LinearFilter; + customizedCubeTexture.generateMipmaps = false; + customizedCubeTexture.needsUpdate = true; + + return customizedCubeTexture; +} + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 500; + + scene = new THREE.Scene(); + + loadCubeTextureWithMipmaps().then(function (cubeTexture) { + //model + const sphere = new THREE.SphereGeometry(100, 128, 128); + + //manual mipmaps + let material = new THREE.MeshBasicMaterial({ color: 0xffffff, envMap: cubeTexture }); + material.name = 'manual mipmaps'; + + let mesh = new THREE.Mesh(sphere, material); + mesh.position.set(100, 0, 0); + scene.add(mesh); + + //webgl mipmaps + material = material.clone(); + material.name = 'auto mipmaps'; + + const autoCubeTexture = cubeTexture.clone(); + autoCubeTexture.mipmaps = []; + autoCubeTexture.generateMipmaps = true; + autoCubeTexture.needsUpdate = true; + + material.envMap = autoCubeTexture; + + mesh = new THREE.Mesh(sphere, material); + mesh.position.set(-100, 0, 0); + scene.add(mesh); + }); + + //renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + //controls + const controls = new OrbitControls(camera, renderer.domElement); + controls.minPolarAngle = Math.PI / 4; + controls.maxPolarAngle = Math.PI / 1.5; + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_cubemap_refraction.ts b/examples-testing/examples/webgl_materials_cubemap_refraction.ts new file mode 100644 index 000000000..8c025071f --- /dev/null +++ b/examples-testing/examples/webgl_materials_cubemap_refraction.ts @@ -0,0 +1,126 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { PLYLoader } from 'three/addons/loaders/PLYLoader.js'; + +let container, stats; + +let camera, scene, renderer; + +let mouseX = 0, + mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 100000); + camera.position.z = -4000; + + // + + const r = 'textures/cube/Park3Med/'; + + const urls = [r + 'px.jpg', r + 'nx.jpg', r + 'py.jpg', r + 'ny.jpg', r + 'pz.jpg', r + 'nz.jpg']; + + const textureCube = new THREE.CubeTextureLoader().load(urls); + textureCube.mapping = THREE.CubeRefractionMapping; + + scene = new THREE.Scene(); + scene.background = textureCube; + + // LIGHTS + + const ambient = new THREE.AmbientLight(0xffffff, 3.5); + scene.add(ambient); + + // material samples + + const cubeMaterial3 = new THREE.MeshPhongMaterial({ + color: 0xccddff, + envMap: textureCube, + refractionRatio: 0.98, + reflectivity: 0.9, + }); + const cubeMaterial2 = new THREE.MeshPhongMaterial({ color: 0xccfffd, envMap: textureCube, refractionRatio: 0.985 }); + const cubeMaterial1 = new THREE.MeshPhongMaterial({ color: 0xffffff, envMap: textureCube, refractionRatio: 0.98 }); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + const loader = new PLYLoader(); + loader.load('models/ply/binary/Lucy100k.ply', function (geometry) { + createScene(geometry, cubeMaterial1, cubeMaterial2, cubeMaterial3); + }); + + document.addEventListener('mousemove', onDocumentMouseMove); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function createScene(geometry, m1, m2, m3) { + geometry.computeVertexNormals(); + + const s = 1.5; + + let mesh = new THREE.Mesh(geometry, m1); + mesh.scale.x = mesh.scale.y = mesh.scale.z = s; + scene.add(mesh); + + mesh = new THREE.Mesh(geometry, m2); + mesh.position.x = -1500; + mesh.scale.x = mesh.scale.y = mesh.scale.z = s; + scene.add(mesh); + + mesh = new THREE.Mesh(geometry, m3); + mesh.position.x = 1500; + mesh.scale.x = mesh.scale.y = mesh.scale.z = s; + scene.add(mesh); +} + +function onDocumentMouseMove(event) { + mouseX = (event.clientX - windowHalfX) * 4; + mouseY = (event.clientY - windowHalfY) * 4; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-mouseY - camera.position.y) * 0.05; + + camera.lookAt(scene.position); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_cubemap_render_to_mipmaps.ts b/examples-testing/examples/webgl_materials_cubemap_render_to_mipmaps.ts new file mode 100644 index 000000000..599a1369b --- /dev/null +++ b/examples-testing/examples/webgl_materials_cubemap_render_to_mipmaps.ts @@ -0,0 +1,183 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let container; +let camera, scene, renderer; + +const CubemapFilterShader = { + name: 'CubemapFilterShader', + + uniforms: { + cubeTexture: { value: null }, + mipIndex: { value: 0 }, + }, + + vertexShader: /* glsl */ ` + + varying vec3 vWorldDirection; + + #include + + void main() { + vWorldDirection = transformDirection(position, modelMatrix); + #include + #include + gl_Position.z = gl_Position.w; // set z to camera.far + } + + `, + + fragmentShader: /* glsl */ ` + + uniform samplerCube cubeTexture; + varying vec3 vWorldDirection; + + uniform float mipIndex; + + #include + + void main() { + vec3 cubeCoordinates = normalize(vWorldDirection); + + // Colorize mip levels + vec4 color = vec4(1.0, 0.0, 0.0, 1.0); + if (mipIndex == 0.0) color.rgb = vec3(1.0, 1.0, 1.0); + else if (mipIndex == 1.0) color.rgb = vec3(0.0, 0.0, 1.0); + else if (mipIndex == 2.0) color.rgb = vec3(0.0, 1.0, 1.0); + else if (mipIndex == 3.0) color.rgb = vec3(0.0, 1.0, 0.0); + else if (mipIndex == 4.0) color.rgb = vec3(1.0, 1.0, 0.0); + + gl_FragColor = textureCube(cubeTexture, cubeCoordinates, 0.0) * color; + } + + `, +}; + +init(); + +async function loadCubeTexture(urls) { + return new Promise(function (resolve) { + new THREE.CubeTextureLoader().load(urls, function (cubeTexture) { + resolve(cubeTexture); + }); + }); +} + +function allocateCubemapRenderTarget(cubeMapSize) { + const params = { + magFilter: THREE.LinearFilter, + minFilter: THREE.LinearMipMapLinearFilter, + generateMipmaps: false, + type: THREE.HalfFloatType, + format: THREE.RGBAFormat, + colorSpace: THREE.LinearSRGBColorSpace, + depthBuffer: false, + }; + + const rt = new THREE.WebGLCubeRenderTarget(cubeMapSize, params); + + const mipLevels = Math.log(cubeMapSize) * Math.LOG2E + 1.0; + for (let i = 0; i < mipLevels; i++) rt.texture.mipmaps.push({}); + + rt.texture.mapping = THREE.CubeReflectionMapping; + return rt; +} + +function renderToCubeTexture(cubeMapRenderTarget, sourceCubeTexture) { + const geometry = new THREE.BoxGeometry(5, 5, 5); + + const material = new THREE.ShaderMaterial({ + name: CubemapFilterShader.name, + uniforms: THREE.UniformsUtils.clone(CubemapFilterShader.uniforms), + vertexShader: CubemapFilterShader.vertexShader, + fragmentShader: CubemapFilterShader.fragmentShader, + side: THREE.BackSide, + blending: THREE.NoBlending, + }); + + material.uniforms.cubeTexture.value = sourceCubeTexture; + + const mesh = new THREE.Mesh(geometry, material); + const cubeCamera = new THREE.CubeCamera(1, 10, cubeMapRenderTarget); + const mipmapCount = Math.floor(Math.log2(Math.max(cubeMapRenderTarget.width, cubeMapRenderTarget.height))); + + for (let mipmap = 0; mipmap < mipmapCount; mipmap++) { + material.uniforms.mipIndex.value = mipmap; + material.needsUpdate = true; + + cubeMapRenderTarget.viewport.set( + 0, + 0, + cubeMapRenderTarget.width >> mipmap, + cubeMapRenderTarget.height >> mipmap, + ); + + cubeCamera.activeMipmapLevel = mipmap; + cubeCamera.update(renderer, mesh); + } + + mesh.geometry.dispose(); + mesh.material.dispose(); +} + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + // Create renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 500; + + // Create controls + const controls = new OrbitControls(camera, renderer.domElement); + controls.minPolarAngle = Math.PI / 4; + controls.maxPolarAngle = Math.PI / 1.5; + + window.addEventListener('resize', onWindowResize); + + // Load a cube texture + const r = 'textures/cube/Park3Med/'; + const urls = [r + 'px.jpg', r + 'nx.jpg', r + 'py.jpg', r + 'ny.jpg', r + 'pz.jpg', r + 'nz.jpg']; + + loadCubeTexture(urls).then(cubeTexture => { + // Allocate a cube map render target + const cubeMapRenderTarget = allocateCubemapRenderTarget(512); + + // Render to all the mip levels of cubeMapRenderTarget + renderToCubeTexture(cubeMapRenderTarget, cubeTexture); + + // Create geometry + const sphere = new THREE.SphereGeometry(100, 128, 128); + let material = new THREE.MeshBasicMaterial({ color: 0xffffff, envMap: cubeTexture }); + + let mesh = new THREE.Mesh(sphere, material); + mesh.position.set(-100, 0, 0); + scene.add(mesh); + + material = material.clone(); + material.envMap = cubeMapRenderTarget.texture; + + mesh = new THREE.Mesh(sphere, material); + mesh.position.set(100, 0, 0); + scene.add(mesh); + }); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_displacementmap.ts b/examples-testing/examples/webgl_materials_displacementmap.ts new file mode 100644 index 000000000..fd0be9a5e --- /dev/null +++ b/examples-testing/examples/webgl_materials_displacementmap.ts @@ -0,0 +1,224 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'; + +let stats; +let camera, scene, renderer, controls; + +const settings = { + metalness: 1.0, + roughness: 0.4, + ambientIntensity: 0.2, + aoMapIntensity: 1.0, + envMapIntensity: 1.0, + displacementScale: 2.436143, // from original model + normalScale: 1.0, +}; + +let mesh, material; + +let pointLight, ambientLight; + +const height = 500; // of camera frustum + +let r = 0.0; + +init(); +initGui(); + +// Init gui +function initGui() { + const gui = new GUI(); + //let gui = gui.addFolder( "Material" ); + gui.add(settings, 'metalness') + .min(0) + .max(1) + .onChange(function (value) { + material.metalness = value; + }); + + gui.add(settings, 'roughness') + .min(0) + .max(1) + .onChange(function (value) { + material.roughness = value; + }); + + gui.add(settings, 'aoMapIntensity') + .min(0) + .max(1) + .onChange(function (value) { + material.aoMapIntensity = value; + }); + + gui.add(settings, 'ambientIntensity') + .min(0) + .max(1) + .onChange(function (value) { + ambientLight.intensity = value; + }); + + gui.add(settings, 'envMapIntensity') + .min(0) + .max(3) + .onChange(function (value) { + material.envMapIntensity = value; + }); + + gui.add(settings, 'displacementScale') + .min(0) + .max(3.0) + .onChange(function (value) { + material.displacementScale = value; + }); + + gui.add(settings, 'normalScale') + .min(-1) + .max(1) + .onChange(function (value) { + material.normalScale.set(1, -1).multiplyScalar(value); + }); +} + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + scene = new THREE.Scene(); + + const aspect = window.innerWidth / window.innerHeight; + camera = new THREE.OrthographicCamera(-height * aspect, height * aspect, height, -height, 1, 10000); + camera.position.z = 1500; + scene.add(camera); + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableZoom = false; + controls.enableDamping = true; + + // lights + + ambientLight = new THREE.AmbientLight(0xffffff, settings.ambientIntensity); + scene.add(ambientLight); + + pointLight = new THREE.PointLight(0xff0000, 1.5, 0, 0); + pointLight.position.z = 2500; + scene.add(pointLight); + + const pointLight2 = new THREE.PointLight(0xff6666, 3, 0, 0); + camera.add(pointLight2); + + const pointLight3 = new THREE.PointLight(0x0000ff, 1.5, 0, 0); + pointLight3.position.x = -1000; + pointLight3.position.z = 1000; + scene.add(pointLight3); + + // env map + + const path = 'textures/cube/SwedishRoyalCastle/'; + const format = '.jpg'; + const urls = [ + path + 'px' + format, + path + 'nx' + format, + path + 'py' + format, + path + 'ny' + format, + path + 'pz' + format, + path + 'nz' + format, + ]; + + const reflectionCube = new THREE.CubeTextureLoader().load(urls); + + // textures + + const textureLoader = new THREE.TextureLoader(); + const normalMap = textureLoader.load('models/obj/ninja/normal.png'); + const aoMap = textureLoader.load('models/obj/ninja/ao.jpg'); + const displacementMap = textureLoader.load('models/obj/ninja/displacement.jpg'); + + // material + + material = new THREE.MeshStandardMaterial({ + color: 0xc1c1c1, + roughness: settings.roughness, + metalness: settings.metalness, + + normalMap: normalMap, + normalScale: new THREE.Vector2(1, -1), // why does the normal map require negation in this case? + + aoMap: aoMap, + aoMapIntensity: 1, + + displacementMap: displacementMap, + displacementScale: settings.displacementScale, + displacementBias: -0.428408, // from original model + + envMap: reflectionCube, + envMapIntensity: settings.envMapIntensity, + + side: THREE.DoubleSide, + }); + + // + + const loader = new OBJLoader(); + loader.load('models/obj/ninja/ninjaHead_Low.obj', function (group) { + const geometry = group.children[0].geometry; + geometry.center(); + + mesh = new THREE.Mesh(geometry, material); + mesh.scale.multiplyScalar(25); + scene.add(mesh); + }); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const aspect = window.innerWidth / window.innerHeight; + + camera.left = -height * aspect; + camera.right = height * aspect; + camera.top = height; + camera.bottom = -height; + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + controls.update(); + + stats.begin(); + render(); + stats.end(); +} + +function render() { + pointLight.position.x = 2500 * Math.cos(r); + pointLight.position.z = 2500 * Math.sin(r); + + r += 0.01; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_envmaps.ts b/examples-testing/examples/webgl_materials_envmaps.ts new file mode 100644 index 000000000..18a5542ed --- /dev/null +++ b/examples-testing/examples/webgl_materials_envmaps.ts @@ -0,0 +1,131 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let controls, camera, scene, renderer; +let textureEquirec, textureCube; +let sphereMesh, sphereMaterial, params; + +init(); + +function init() { + // CAMERAS + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0, 0, 2.5); + + // SCENE + + scene = new THREE.Scene(); + + // Textures + + const loader = new THREE.CubeTextureLoader(); + loader.setPath('textures/cube/Bridge2/'); + + textureCube = loader.load(['posx.jpg', 'negx.jpg', 'posy.jpg', 'negy.jpg', 'posz.jpg', 'negz.jpg']); + + const textureLoader = new THREE.TextureLoader(); + + textureEquirec = textureLoader.load('textures/2294472375_24a3b8ef46_o.jpg'); + textureEquirec.mapping = THREE.EquirectangularReflectionMapping; + textureEquirec.colorSpace = THREE.SRGBColorSpace; + + scene.background = textureCube; + + // + + const geometry = new THREE.IcosahedronGeometry(1, 15); + sphereMaterial = new THREE.MeshBasicMaterial({ envMap: textureCube }); + sphereMesh = new THREE.Mesh(geometry, sphereMaterial); + scene.add(sphereMesh); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 1.5; + controls.maxDistance = 6; + + // + + params = { + Cube: function () { + scene.background = textureCube; + + sphereMaterial.envMap = textureCube; + sphereMaterial.needsUpdate = true; + }, + Equirectangular: function () { + scene.background = textureEquirec; + + sphereMaterial.envMap = textureEquirec; + sphereMaterial.needsUpdate = true; + }, + Refraction: false, + backgroundRotationX: false, + backgroundRotationY: false, + backgroundRotationZ: false, + syncMaterial: false, + }; + + const gui = new GUI({ width: 300 }); + gui.add(params, 'Cube'); + gui.add(params, 'Equirectangular'); + gui.add(params, 'Refraction').onChange(function (value) { + if (value) { + textureEquirec.mapping = THREE.EquirectangularRefractionMapping; + textureCube.mapping = THREE.CubeRefractionMapping; + } else { + textureEquirec.mapping = THREE.EquirectangularReflectionMapping; + textureCube.mapping = THREE.CubeReflectionMapping; + } + + sphereMaterial.needsUpdate = true; + }); + gui.add(params, 'backgroundRotationX'); + gui.add(params, 'backgroundRotationY'); + gui.add(params, 'backgroundRotationZ'); + gui.add(params, 'syncMaterial'); + gui.open(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + if (params.backgroundRotationX) { + scene.backgroundRotation.x += 0.001; + } + + if (params.backgroundRotationY) { + scene.backgroundRotation.y += 0.001; + } + + if (params.backgroundRotationZ) { + scene.backgroundRotation.z += 0.001; + } + + if (params.syncMaterial) { + sphereMesh.material.envMapRotation.copy(scene.backgroundRotation); + } + + camera.lookAt(scene.position); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_envmaps_exr.ts b/examples-testing/examples/webgl_materials_envmaps_exr.ts new file mode 100644 index 000000000..c3f3f4f7d --- /dev/null +++ b/examples-testing/examples/webgl_materials_envmaps_exr.ts @@ -0,0 +1,153 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { EXRLoader } from 'three/addons/loaders/EXRLoader.js'; + +const params = { + envMap: 'EXR', + roughness: 0.0, + metalness: 0.0, + exposure: 1.0, + debug: false, +}; + +let container, stats; +let camera, scene, renderer, controls; +let torusMesh, planeMesh; +let pngCubeRenderTarget, exrCubeRenderTarget; +let pngBackground, exrBackground; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 0, 120); + + scene = new THREE.Scene(); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + + container.appendChild(renderer.domElement); + + renderer.toneMapping = THREE.ACESFilmicToneMapping; + + // + + let geometry = new THREE.TorusKnotGeometry(18, 8, 150, 20); + let material = new THREE.MeshStandardMaterial({ + metalness: params.metalness, + roughness: params.roughness, + envMapIntensity: 1.0, + }); + + torusMesh = new THREE.Mesh(geometry, material); + scene.add(torusMesh); + + geometry = new THREE.PlaneGeometry(200, 200); + material = new THREE.MeshBasicMaterial(); + + planeMesh = new THREE.Mesh(geometry, material); + planeMesh.position.y = -50; + planeMesh.rotation.x = -Math.PI * 0.5; + scene.add(planeMesh); + + THREE.DefaultLoadingManager.onLoad = function () { + pmremGenerator.dispose(); + }; + + new EXRLoader().load('textures/piz_compressed.exr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + exrCubeRenderTarget = pmremGenerator.fromEquirectangular(texture); + exrBackground = texture; + }); + + new THREE.TextureLoader().load('textures/equirectangular.png', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + texture.colorSpace = THREE.SRGBColorSpace; + + pngCubeRenderTarget = pmremGenerator.fromEquirectangular(texture); + pngBackground = texture; + }); + + const pmremGenerator = new THREE.PMREMGenerator(renderer); + pmremGenerator.compileEquirectangularShader(); + + stats = new Stats(); + container.appendChild(stats.dom); + + controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 50; + controls.maxDistance = 300; + + window.addEventListener('resize', onWindowResize); + + const gui = new GUI(); + + gui.add(params, 'envMap', ['EXR', 'PNG']); + gui.add(params, 'roughness', 0, 1, 0.01); + gui.add(params, 'metalness', 0, 1, 0.01); + gui.add(params, 'exposure', 0, 2, 0.01); + gui.add(params, 'debug'); + gui.open(); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); +} + +function animate() { + stats.begin(); + render(); + stats.end(); +} + +function render() { + torusMesh.material.roughness = params.roughness; + torusMesh.material.metalness = params.metalness; + + let newEnvMap = torusMesh.material.envMap; + let background = scene.background; + + switch (params.envMap) { + case 'EXR': + newEnvMap = exrCubeRenderTarget ? exrCubeRenderTarget.texture : null; + background = exrBackground; + break; + case 'PNG': + newEnvMap = pngCubeRenderTarget ? pngCubeRenderTarget.texture : null; + background = pngBackground; + break; + } + + if (newEnvMap !== torusMesh.material.envMap) { + torusMesh.material.envMap = newEnvMap; + torusMesh.material.needsUpdate = true; + + planeMesh.material.map = newEnvMap; + planeMesh.material.needsUpdate = true; + } + + torusMesh.rotation.y += 0.005; + planeMesh.visible = params.debug; + + scene.background = background; + renderer.toneMappingExposure = params.exposure; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_envmaps_groundprojected.ts b/examples-testing/examples/webgl_materials_envmaps_groundprojected.ts new file mode 100644 index 000000000..48e0077f4 --- /dev/null +++ b/examples-testing/examples/webgl_materials_envmaps_groundprojected.ts @@ -0,0 +1,150 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GroundedSkybox } from 'three/addons/objects/GroundedSkybox.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +const params = { + height: 15, + radius: 100, + enabled: true, +}; + +let camera, scene, renderer, skybox; + +init().then(render); + +async function init() { + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(-20, 7, 20); + camera.lookAt(0, 4, 0); + + scene = new THREE.Scene(); + + const hdrLoader = new RGBELoader(); + const envMap = await hdrLoader.loadAsync('textures/equirectangular/blouberg_sunrise_2_1k.hdr'); + envMap.mapping = THREE.EquirectangularReflectionMapping; + + skybox = new GroundedSkybox(envMap, params.height, params.radius); + skybox.position.y = params.height - 0.01; + scene.add(skybox); + + scene.environment = envMap; + + const dracoLoader = new DRACOLoader(); + dracoLoader.setDecoderPath('jsm/libs/draco/gltf/'); + + const loader = new GLTFLoader(); + loader.setDRACOLoader(dracoLoader); + + const shadow = new THREE.TextureLoader().load('models/gltf/ferrari_ao.png'); + + loader.load('models/gltf/ferrari.glb', function (gltf) { + const bodyMaterial = new THREE.MeshPhysicalMaterial({ + color: 0x000000, + metalness: 1.0, + roughness: 0.8, + clearcoat: 1.0, + clearcoatRoughness: 0.2, + }); + + const detailsMaterial = new THREE.MeshStandardMaterial({ + color: 0xffffff, + metalness: 1.0, + roughness: 0.5, + }); + + const glassMaterial = new THREE.MeshPhysicalMaterial({ + color: 0xffffff, + metalness: 0.25, + roughness: 0, + transmission: 1.0, + }); + + const carModel = gltf.scene.children[0]; + carModel.scale.multiplyScalar(4); + carModel.rotation.y = Math.PI; + + carModel.getObjectByName('body').material = bodyMaterial; + + carModel.getObjectByName('rim_fl').material = detailsMaterial; + carModel.getObjectByName('rim_fr').material = detailsMaterial; + carModel.getObjectByName('rim_rr').material = detailsMaterial; + carModel.getObjectByName('rim_rl').material = detailsMaterial; + carModel.getObjectByName('trim').material = detailsMaterial; + + carModel.getObjectByName('glass').material = glassMaterial; + + // shadow + const mesh = new THREE.Mesh( + new THREE.PlaneGeometry(0.655 * 4, 1.3 * 4), + new THREE.MeshBasicMaterial({ + map: shadow, + blending: THREE.MultiplyBlending, + toneMapped: false, + transparent: true, + }), + ); + mesh.rotation.x = -Math.PI / 2; + carModel.add(mesh); + + scene.add(carModel); + + render(); + }); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); + controls.target.set(0, 2, 0); + controls.maxPolarAngle = THREE.MathUtils.degToRad(90); + controls.maxDistance = 80; + controls.minDistance = 20; + controls.enablePan = false; + controls.update(); + + document.body.appendChild(renderer.domElement); + window.addEventListener('resize', onWindowResize); + + const gui = new GUI(); + + gui.add(params, 'enabled') + .name('Grounded') + .onChange(function (value) { + if (value) { + scene.add(skybox); + scene.background = null; + } else { + scene.remove(skybox); + scene.background = scene.environment; + } + + render(); + }); + + gui.open(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_envmaps_hdr.ts b/examples-testing/examples/webgl_materials_envmaps_hdr.ts new file mode 100644 index 000000000..b4c6f64ef --- /dev/null +++ b/examples-testing/examples/webgl_materials_envmaps_hdr.ts @@ -0,0 +1,176 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { HDRCubeTextureLoader } from 'three/addons/loaders/HDRCubeTextureLoader.js'; +import { RGBMLoader } from 'three/addons/loaders/RGBMLoader.js'; +import { DebugEnvironment } from 'three/addons/environments/DebugEnvironment.js'; + +const params = { + envMap: 'HDR', + roughness: 0.0, + metalness: 0.0, + exposure: 1.0, + debug: false, +}; + +let container, stats; +let camera, scene, renderer, controls; +let torusMesh, planeMesh; +let generatedCubeRenderTarget, ldrCubeRenderTarget, hdrCubeRenderTarget, rgbmCubeRenderTarget; +let ldrCubeMap, hdrCubeMap, rgbmCubeMap; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 0, 120); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x000000); + + renderer = new THREE.WebGLRenderer(); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + + // + + let geometry = new THREE.TorusKnotGeometry(18, 8, 150, 20); + // let geometry = new THREE.SphereGeometry( 26, 64, 32 ); + let material = new THREE.MeshStandardMaterial({ + color: 0xffffff, + metalness: params.metalness, + roughness: params.roughness, + }); + + torusMesh = new THREE.Mesh(geometry, material); + scene.add(torusMesh); + + geometry = new THREE.PlaneGeometry(200, 200); + material = new THREE.MeshBasicMaterial(); + + planeMesh = new THREE.Mesh(geometry, material); + planeMesh.position.y = -50; + planeMesh.rotation.x = -Math.PI * 0.5; + scene.add(planeMesh); + + THREE.DefaultLoadingManager.onLoad = function () { + pmremGenerator.dispose(); + }; + + const hdrUrls = ['px.hdr', 'nx.hdr', 'py.hdr', 'ny.hdr', 'pz.hdr', 'nz.hdr']; + hdrCubeMap = new HDRCubeTextureLoader().setPath('./textures/cube/pisaHDR/').load(hdrUrls, function () { + hdrCubeRenderTarget = pmremGenerator.fromCubemap(hdrCubeMap); + + hdrCubeMap.magFilter = THREE.LinearFilter; + hdrCubeMap.needsUpdate = true; + }); + + const ldrUrls = ['px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png']; + ldrCubeMap = new THREE.CubeTextureLoader().setPath('./textures/cube/pisa/').load(ldrUrls, function () { + ldrCubeRenderTarget = pmremGenerator.fromCubemap(ldrCubeMap); + }); + + const rgbmUrls = ['px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png']; + rgbmCubeMap = new RGBMLoader() + .setMaxRange(16) + .setPath('./textures/cube/pisaRGBM16/') + .loadCubemap(rgbmUrls, function () { + rgbmCubeRenderTarget = pmremGenerator.fromCubemap(rgbmCubeMap); + }); + + const pmremGenerator = new THREE.PMREMGenerator(renderer); + pmremGenerator.compileCubemapShader(); + + const envScene = new DebugEnvironment(); + generatedCubeRenderTarget = pmremGenerator.fromScene(envScene); + + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + //renderer.toneMapping = ReinhardToneMapping; + + stats = new Stats(); + container.appendChild(stats.dom); + + controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 50; + controls.maxDistance = 300; + + window.addEventListener('resize', onWindowResize); + + const gui = new GUI(); + + gui.add(params, 'envMap', ['Generated', 'LDR', 'HDR', 'RGBM16']); + gui.add(params, 'roughness', 0, 1, 0.01); + gui.add(params, 'metalness', 0, 1, 0.01); + gui.add(params, 'exposure', 0, 2, 0.01); + gui.add(params, 'debug'); + gui.open(); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); +} + +function animate() { + stats.begin(); + render(); + stats.end(); +} + +function render() { + torusMesh.material.roughness = params.roughness; + torusMesh.material.metalness = params.metalness; + + let renderTarget, cubeMap; + + switch (params.envMap) { + case 'Generated': + renderTarget = generatedCubeRenderTarget; + cubeMap = generatedCubeRenderTarget.texture; + break; + case 'LDR': + renderTarget = ldrCubeRenderTarget; + cubeMap = ldrCubeMap; + break; + case 'HDR': + renderTarget = hdrCubeRenderTarget; + cubeMap = hdrCubeMap; + break; + case 'RGBM16': + renderTarget = rgbmCubeRenderTarget; + cubeMap = rgbmCubeMap; + break; + } + + const newEnvMap = renderTarget ? renderTarget.texture : null; + + if (newEnvMap && newEnvMap !== torusMesh.material.envMap) { + torusMesh.material.envMap = newEnvMap; + torusMesh.material.needsUpdate = true; + + planeMesh.material.map = newEnvMap; + planeMesh.material.needsUpdate = true; + } + + torusMesh.rotation.y += 0.005; + planeMesh.visible = params.debug; + + scene.background = cubeMap; + renderer.toneMappingExposure = params.exposure; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_modified.ts b/examples-testing/examples/webgl_materials_modified.ts new file mode 100644 index 000000000..c6ee5af3c --- /dev/null +++ b/examples-testing/examples/webgl_materials_modified.ts @@ -0,0 +1,115 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +let camera, scene, renderer, stats; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.z = 20; + + scene = new THREE.Scene(); + + const loader = new GLTFLoader(); + loader.load('models/gltf/LeePerrySmith/LeePerrySmith.glb', function (gltf) { + const geometry = gltf.scene.children[0].geometry; + + let mesh = new THREE.Mesh(geometry, buildTwistMaterial(2.0)); + mesh.position.x = -3.5; + mesh.position.y = -0.5; + scene.add(mesh); + + mesh = new THREE.Mesh(geometry, buildTwistMaterial(-2.0)); + mesh.position.x = 3.5; + mesh.position.y = -0.5; + scene.add(mesh); + }); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 10; + controls.maxDistance = 50; + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // EVENTS + + window.addEventListener('resize', onWindowResize); +} + +function buildTwistMaterial(amount) { + const material = new THREE.MeshNormalMaterial(); + material.onBeforeCompile = function (shader) { + shader.uniforms.time = { value: 0 }; + + shader.vertexShader = 'uniform float time;\n' + shader.vertexShader; + shader.vertexShader = shader.vertexShader.replace( + '#include ', + [ + `float theta = sin( time + position.y ) / ${amount.toFixed(1)};`, + 'float c = cos( theta );', + 'float s = sin( theta );', + 'mat3 m = mat3( c, 0, s, 0, 1, 0, -s, 0, c );', + 'vec3 transformed = vec3( position ) * m;', + 'vNormal = vNormal * m;', + ].join('\n'), + ); + + material.userData.shader = shader; + }; + + // Make sure WebGLRenderer doesnt reuse a single program + + material.customProgramCacheKey = function () { + return amount.toFixed(1); + }; + + return material; +} + +// + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); +} + +// + +function animate() { + render(); + + stats.update(); +} + +function render() { + scene.traverse(function (child) { + if (child.isMesh) { + const shader = child.material.userData.shader; + + if (shader) { + shader.uniforms.time.value = performance.now() / 1000; + } + } + }); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_normalmap_object_space.ts b/examples-testing/examples/webgl_materials_normalmap_object_space.ts new file mode 100644 index 000000000..1fc6f8066 --- /dev/null +++ b/examples-testing/examples/webgl_materials_normalmap_object_space.ts @@ -0,0 +1,82 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +let renderer, scene, camera; + +init(); + +function init() { + // renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + // scene + scene = new THREE.Scene(); + + // camera + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(-10, 0, 23); + scene.add(camera); + + // controls + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); + controls.minDistance = 10; + controls.maxDistance = 50; + controls.enablePan = false; + + // ambient + scene.add(new THREE.AmbientLight(0xffffff, 0.6)); + + // light + const light = new THREE.PointLight(0xffffff, 4.5, 0, 0); + camera.add(light); + + // model + new GLTFLoader().load('models/gltf/Nefertiti/Nefertiti.glb', function (gltf) { + gltf.scene.traverse(function (child) { + if (child.isMesh) { + // glTF currently supports only tangent-space normal maps. + // this model has been modified to demonstrate the use of an object-space normal map. + + child.material.normalMapType = THREE.ObjectSpaceNormalMap; + + // attribute normals are not required with an object-space normal map. remove them. + + child.geometry.deleteAttribute('normal'); + + // + + child.material.side = THREE.DoubleSide; + + child.scale.multiplyScalar(0.5); + + // recenter + + new THREE.Box3().setFromObject(child).getCenter(child.position).multiplyScalar(-1); + + scene.add(child); + } + }); + + render(); + }); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_physical_clearcoat.ts b/examples-testing/examples/webgl_materials_physical_clearcoat.ts new file mode 100644 index 000000000..408fd9921 --- /dev/null +++ b/examples-testing/examples/webgl_materials_physical_clearcoat.ts @@ -0,0 +1,208 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { HDRCubeTextureLoader } from 'three/addons/loaders/HDRCubeTextureLoader.js'; + +import { FlakesTexture } from 'three/addons/textures/FlakesTexture.js'; + +let container, stats; + +let camera, scene, renderer; + +let particleLight; +let group; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 0.25, 50); + camera.position.z = 10; + + scene = new THREE.Scene(); + + group = new THREE.Group(); + scene.add(group); + + new HDRCubeTextureLoader() + .setPath('textures/cube/pisaHDR/') + .load(['px.hdr', 'nx.hdr', 'py.hdr', 'ny.hdr', 'pz.hdr', 'nz.hdr'], function (texture) { + const geometry = new THREE.SphereGeometry(0.8, 64, 32); + + const textureLoader = new THREE.TextureLoader(); + + const diffuse = textureLoader.load('textures/carbon/Carbon.png'); + diffuse.colorSpace = THREE.SRGBColorSpace; + diffuse.wrapS = THREE.RepeatWrapping; + diffuse.wrapT = THREE.RepeatWrapping; + diffuse.repeat.x = 10; + diffuse.repeat.y = 10; + + const normalMap = textureLoader.load('textures/carbon/Carbon_Normal.png'); + normalMap.wrapS = THREE.RepeatWrapping; + normalMap.wrapT = THREE.RepeatWrapping; + normalMap.repeat.x = 10; + normalMap.repeat.y = 10; + + const normalMap2 = textureLoader.load('textures/water/Water_1_M_Normal.jpg'); + + const normalMap3 = new THREE.CanvasTexture(new FlakesTexture()); + normalMap3.wrapS = THREE.RepeatWrapping; + normalMap3.wrapT = THREE.RepeatWrapping; + normalMap3.repeat.x = 10; + normalMap3.repeat.y = 6; + normalMap3.anisotropy = 16; + + const normalMap4 = textureLoader.load('textures/golfball.jpg'); + + const clearcoatNormalMap = textureLoader.load( + 'textures/pbr/Scratched_gold/Scratched_gold_01_1K_Normal.png', + ); + + // car paint + + let material = new THREE.MeshPhysicalMaterial({ + clearcoat: 1.0, + clearcoatRoughness: 0.1, + metalness: 0.9, + roughness: 0.5, + color: 0x0000ff, + normalMap: normalMap3, + normalScale: new THREE.Vector2(0.15, 0.15), + }); + + let mesh = new THREE.Mesh(geometry, material); + mesh.position.x = -1; + mesh.position.y = 1; + group.add(mesh); + + // fibers + + material = new THREE.MeshPhysicalMaterial({ + roughness: 0.5, + clearcoat: 1.0, + clearcoatRoughness: 0.1, + map: diffuse, + normalMap: normalMap, + }); + mesh = new THREE.Mesh(geometry, material); + mesh.position.x = 1; + mesh.position.y = 1; + group.add(mesh); + + // golf + + material = new THREE.MeshPhysicalMaterial({ + metalness: 0.0, + roughness: 0.1, + clearcoat: 1.0, + normalMap: normalMap4, + clearcoatNormalMap: clearcoatNormalMap, + + // y scale is negated to compensate for normal map handedness. + clearcoatNormalScale: new THREE.Vector2(2.0, -2.0), + }); + mesh = new THREE.Mesh(geometry, material); + mesh.position.x = -1; + mesh.position.y = -1; + group.add(mesh); + + // clearcoat + normalmap + + material = new THREE.MeshPhysicalMaterial({ + clearcoat: 1.0, + metalness: 1.0, + color: 0xff0000, + normalMap: normalMap2, + normalScale: new THREE.Vector2(0.15, 0.15), + clearcoatNormalMap: clearcoatNormalMap, + + // y scale is negated to compensate for normal map handedness. + clearcoatNormalScale: new THREE.Vector2(2.0, -2.0), + }); + mesh = new THREE.Mesh(geometry, material); + mesh.position.x = 1; + mesh.position.y = -1; + group.add(mesh); + + // + + scene.background = texture; + scene.environment = texture; + }); + + // LIGHTS + + particleLight = new THREE.Mesh( + new THREE.SphereGeometry(0.05, 8, 8), + new THREE.MeshBasicMaterial({ color: 0xffffff }), + ); + scene.add(particleLight); + + particleLight.add(new THREE.PointLight(0xffffff, 30)); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1.25; + + // + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // EVENTS + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 3; + controls.maxDistance = 30; + + window.addEventListener('resize', onWindowResize); +} + +// + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); +} + +// + +function animate() { + render(); + + stats.update(); +} + +function render() { + const timer = Date.now() * 0.00025; + + particleLight.position.x = Math.sin(timer * 7) * 3; + particleLight.position.y = Math.cos(timer * 5) * 4; + particleLight.position.z = Math.cos(timer * 3) * 3; + + for (let i = 0; i < group.children.length; i++) { + const child = group.children[i]; + child.rotation.y += 0.005; + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_physical_transmission.ts b/examples-testing/examples/webgl_materials_physical_transmission.ts new file mode 100644 index 000000000..d45967971 --- /dev/null +++ b/examples-testing/examples/webgl_materials_physical_transmission.ts @@ -0,0 +1,182 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +const params = { + color: 0xffffff, + transmission: 1, + opacity: 1, + metalness: 0, + roughness: 0, + ior: 1.5, + thickness: 0.01, + specularIntensity: 1, + specularColor: 0xffffff, + envMapIntensity: 1, + lightIntensity: 1, + exposure: 1, +}; + +let camera, scene, renderer; + +let mesh; + +const hdrEquirect = new RGBELoader().setPath('textures/equirectangular/').load('royal_esplanade_1k.hdr', function () { + hdrEquirect.mapping = THREE.EquirectangularReflectionMapping; + + init(); + render(); +}); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.shadowMap.enabled = true; + document.body.appendChild(renderer.domElement); + + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = params.exposure; + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 2000); + camera.position.set(0, 0, 120); + + // + + scene.background = hdrEquirect; + + // + + const geometry = new THREE.SphereGeometry(20, 64, 32); + + const texture = new THREE.CanvasTexture(generateTexture()); + texture.magFilter = THREE.NearestFilter; + texture.wrapT = THREE.RepeatWrapping; + texture.wrapS = THREE.RepeatWrapping; + texture.repeat.set(1, 3.5); + + const material = new THREE.MeshPhysicalMaterial({ + color: params.color, + metalness: params.metalness, + roughness: params.roughness, + ior: params.ior, + alphaMap: texture, + envMap: hdrEquirect, + envMapIntensity: params.envMapIntensity, + transmission: params.transmission, // use material.transmission for glass materials + specularIntensity: params.specularIntensity, + specularColor: params.specularColor, + opacity: params.opacity, + side: THREE.DoubleSide, + transparent: true, + }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use if there is no animation loop + controls.minDistance = 10; + controls.maxDistance = 150; + + window.addEventListener('resize', onWindowResize); + + // + + const gui = new GUI(); + + gui.addColor(params, 'color').onChange(function () { + material.color.set(params.color); + render(); + }); + + gui.add(params, 'transmission', 0, 1, 0.01).onChange(function () { + material.transmission = params.transmission; + render(); + }); + + gui.add(params, 'opacity', 0, 1, 0.01).onChange(function () { + material.opacity = params.opacity; + render(); + }); + + gui.add(params, 'metalness', 0, 1, 0.01).onChange(function () { + material.metalness = params.metalness; + render(); + }); + + gui.add(params, 'roughness', 0, 1, 0.01).onChange(function () { + material.roughness = params.roughness; + render(); + }); + + gui.add(params, 'ior', 1, 2, 0.01).onChange(function () { + material.ior = params.ior; + render(); + }); + + gui.add(params, 'thickness', 0, 5, 0.01).onChange(function () { + material.thickness = params.thickness; + render(); + }); + + gui.add(params, 'specularIntensity', 0, 1, 0.01).onChange(function () { + material.specularIntensity = params.specularIntensity; + render(); + }); + + gui.addColor(params, 'specularColor').onChange(function () { + material.specularColor.set(params.specularColor); + render(); + }); + + gui.add(params, 'envMapIntensity', 0, 1, 0.01) + .name('envMap intensity') + .onChange(function () { + material.envMapIntensity = params.envMapIntensity; + render(); + }); + + gui.add(params, 'exposure', 0, 1, 0.01).onChange(function () { + renderer.toneMappingExposure = params.exposure; + render(); + }); + + gui.open(); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + + render(); +} + +// + +function generateTexture() { + const canvas = document.createElement('canvas'); + canvas.width = 2; + canvas.height = 2; + + const context = canvas.getContext('2d'); + context.fillStyle = 'white'; + context.fillRect(0, 1, 2, 1); + + return canvas; +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_physical_transmission_alpha.ts b/examples-testing/examples/webgl_materials_physical_transmission_alpha.ts new file mode 100644 index 000000000..d81f59c37 --- /dev/null +++ b/examples-testing/examples/webgl_materials_physical_transmission_alpha.ts @@ -0,0 +1,192 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +const params = { + color: 0xffffff, + transmission: 1, + opacity: 1, + metalness: 0, + roughness: 0, + ior: 1.5, + thickness: 0.01, + attenuationColor: 0xffffff, + attenuationDistance: 1, + specularIntensity: 1, + specularColor: 0xffffff, + envMapIntensity: 1, + lightIntensity: 1, + exposure: 1, +}; + +let camera, scene, renderer; + +let mesh, material; + +const hdrEquirect = new RGBELoader().setPath('textures/equirectangular/').load('royal_esplanade_1k.hdr', function () { + hdrEquirect.mapping = THREE.EquirectangularReflectionMapping; + + new GLTFLoader().setPath('models/gltf/').load('DragonAttenuation.glb', function (gltf) { + gltf.scene.traverse(function (child) { + if (child.isMesh && child.material.isMeshPhysicalMaterial) { + mesh = child; + material = mesh.material; + + const color = new THREE.Color(); + + params.color = color.copy(mesh.material.color).getHex(); + params.roughness = mesh.material.roughness; + params.metalness = mesh.material.metalness; + + params.ior = mesh.material.ior; + params.specularIntensity = mesh.material.specularIntensity; + + params.transmission = mesh.material.transmission; + params.thickness = mesh.material.thickness; + params.attenuationColor = color.copy(mesh.material.attenuationColor).getHex(); + params.attenuationDistance = mesh.material.attenuationDistance; + } + }); + + init(); + + scene.add(gltf.scene); + + scene.environment = hdrEquirect; + //scene.background = hdrEquirect; + + render(); + }); +}); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.shadowMap.enabled = true; + document.body.appendChild(renderer.domElement); + + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = params.exposure; + + // accommodate CSS table + renderer.domElement.style.position = 'absolute'; + renderer.domElement.style.top = 0; + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 2000); + camera.position.set(-5, 0.5, 0); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use if there is no animation loop + controls.minDistance = 5; + controls.maxDistance = 20; + controls.target.y = 0.5; + controls.update(); + + window.addEventListener('resize', onWindowResize); + + // + + const gui = new GUI(); + + gui.addColor(params, 'color').onChange(function () { + material.color.set(params.color); + render(); + }); + + gui.add(params, 'transmission', 0, 1, 0.01).onChange(function () { + material.transmission = params.transmission; + render(); + }); + + gui.add(params, 'opacity', 0, 1, 0.01).onChange(function () { + material.opacity = params.opacity; + const transparent = params.opacity < 1; + + if (transparent !== material.transparent) { + material.transparent = transparent; + material.needsUpdate = true; + } + + render(); + }); + + gui.add(params, 'metalness', 0, 1, 0.01).onChange(function () { + material.metalness = params.metalness; + render(); + }); + + gui.add(params, 'roughness', 0, 1, 0.01).onChange(function () { + material.roughness = params.roughness; + render(); + }); + + gui.add(params, 'ior', 1, 2, 0.01).onChange(function () { + material.ior = params.ior; + render(); + }); + + gui.add(params, 'thickness', 0, 5, 0.01).onChange(function () { + material.thickness = params.thickness; + render(); + }); + + gui.addColor(params, 'attenuationColor') + .name('attenuation color') + .onChange(function () { + material.attenuationColor.set(params.attenuationColor); + render(); + }); + + gui.add(params, 'attenuationDistance', 0, 1, 0.01).onChange(function () { + material.attenuationDistance = params.attenuationDistance; + render(); + }); + + gui.add(params, 'specularIntensity', 0, 1, 0.01).onChange(function () { + material.specularIntensity = params.specularIntensity; + render(); + }); + + gui.addColor(params, 'specularColor').onChange(function () { + material.specularColor.set(params.specularColor); + render(); + }); + + gui.add(params, 'envMapIntensity', 0, 1, 0.01) + .name('envMap intensity') + .onChange(function () { + material.envMapIntensity = params.envMapIntensity; + render(); + }); + + gui.add(params, 'exposure', 0, 1, 0.01).onChange(function () { + renderer.toneMappingExposure = params.exposure; + render(); + }); + + gui.open(); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + + render(); +} + +// + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_texture_anisotropy.ts b/examples-testing/examples/webgl_materials_texture_anisotropy.ts new file mode 100644 index 000000000..1e030d64d --- /dev/null +++ b/examples-testing/examples/webgl_materials_texture_anisotropy.ts @@ -0,0 +1,143 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +const SCREEN_WIDTH = window.innerWidth; +const SCREEN_HEIGHT = window.innerHeight; + +let container, stats; + +let camera, scene1, scene2, renderer; + +let mouseX = 0, + mouseY = 0; + +const windowHalfX = window.innerWidth / 2; +const windowHalfY = window.innerHeight / 2; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + + // + + camera = new THREE.PerspectiveCamera(35, SCREEN_WIDTH / SCREEN_HEIGHT, 1, 25000); + camera.position.z = 1500; + + scene1 = new THREE.Scene(); + scene1.background = new THREE.Color(0xf2f7ff); + scene1.fog = new THREE.Fog(0xf2f7ff, 1, 25000); + + scene2 = new THREE.Scene(); + scene2.background = new THREE.Color(0xf2f7ff); + scene2.fog = new THREE.Fog(0xf2f7ff, 1, 25000); + + scene1.add(new THREE.AmbientLight(0xeef0ff, 3)); + scene2.add(new THREE.AmbientLight(0xeef0ff, 3)); + + const light1 = new THREE.DirectionalLight(0xffffff, 6); + light1.position.set(1, 1, 1); + scene1.add(light1); + + const light2 = new THREE.DirectionalLight(0xffffff, 6); + light2.position.set(1, 1, 1); + scene2.add(light2); + + // GROUND + + const textureLoader = new THREE.TextureLoader(); + + const maxAnisotropy = renderer.capabilities.getMaxAnisotropy(); + + const texture1 = textureLoader.load('textures/crate.gif'); + const material1 = new THREE.MeshPhongMaterial({ color: 0xffffff, map: texture1 }); + + texture1.colorSpace = THREE.SRGBColorSpace; + texture1.anisotropy = maxAnisotropy; + texture1.wrapS = texture1.wrapT = THREE.RepeatWrapping; + texture1.repeat.set(512, 512); + + const texture2 = textureLoader.load('textures/crate.gif'); + const material2 = new THREE.MeshPhongMaterial({ color: 0xffffff, map: texture2 }); + + texture2.colorSpace = THREE.SRGBColorSpace; + texture2.anisotropy = 1; + texture2.wrapS = texture2.wrapT = THREE.RepeatWrapping; + texture2.repeat.set(512, 512); + + if (maxAnisotropy > 0) { + document.getElementById('val_left').innerHTML = texture1.anisotropy; + document.getElementById('val_right').innerHTML = texture2.anisotropy; + } else { + document.getElementById('val_left').innerHTML = 'not supported'; + document.getElementById('val_right').innerHTML = 'not supported'; + } + + // + + const geometry = new THREE.PlaneGeometry(100, 100); + + const mesh1 = new THREE.Mesh(geometry, material1); + mesh1.rotation.x = -Math.PI / 2; + mesh1.scale.set(1000, 1000, 1000); + + const mesh2 = new THREE.Mesh(geometry, material2); + mesh2.rotation.x = -Math.PI / 2; + mesh2.scale.set(1000, 1000, 1000); + + scene1.add(mesh1); + scene2.add(mesh2); + + // RENDERER + + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + renderer.setAnimationLoop(animate); + renderer.autoClear = false; + + renderer.domElement.style.position = 'relative'; + container.appendChild(renderer.domElement); + + // STATS1 + + stats = new Stats(); + container.appendChild(stats.dom); + + document.addEventListener('mousemove', onDocumentMouseMove); +} + +function onDocumentMouseMove(event) { + mouseX = event.clientX - windowHalfX; + mouseY = event.clientY - windowHalfY; +} + +function animate() { + render(); + stats.update(); +} + +function render() { + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y = THREE.MathUtils.clamp( + camera.position.y + (-(mouseY - 200) - camera.position.y) * 0.05, + 50, + 1000, + ); + + camera.lookAt(scene1.position); + + renderer.clear(); + renderer.setScissorTest(true); + + renderer.setScissor(0, 0, SCREEN_WIDTH / 2 - 2, SCREEN_HEIGHT); + renderer.render(scene1, camera); + + renderer.setScissor(SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2 - 2, SCREEN_HEIGHT); + renderer.render(scene2, camera); + + renderer.setScissorTest(false); +} diff --git a/examples-testing/examples/webgl_materials_texture_canvas.ts b/examples-testing/examples/webgl_materials_texture_canvas.ts new file mode 100644 index 000000000..d23c68436 --- /dev/null +++ b/examples-testing/examples/webgl_materials_texture_canvas.ts @@ -0,0 +1,92 @@ +import * as THREE from 'three'; + +let camera, scene, renderer, mesh, material; +const drawStartPos = new THREE.Vector2(); + +init(); +setupCanvasDrawing(); + +function init() { + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 2000); + camera.position.z = 500; + + scene = new THREE.Scene(); + + material = new THREE.MeshBasicMaterial(); + + mesh = new THREE.Mesh(new THREE.BoxGeometry(200, 200, 200), material); + scene.add(mesh); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); +} + +// Sets up the drawing canvas and adds it as the material map + +function setupCanvasDrawing() { + // get canvas and context + + const drawingCanvas = document.getElementById('drawing-canvas'); + const drawingContext = drawingCanvas.getContext('2d'); + + // draw white background + + drawingContext.fillStyle = '#FFFFFF'; + drawingContext.fillRect(0, 0, 128, 128); + + // set canvas as material.map (this could be done to any map, bump, displacement etc.) + + material.map = new THREE.CanvasTexture(drawingCanvas); + + // set the variable to keep track of when to draw + + let paint = false; + + // add canvas event listeners + drawingCanvas.addEventListener('pointerdown', function (e) { + paint = true; + drawStartPos.set(e.offsetX, e.offsetY); + }); + + drawingCanvas.addEventListener('pointermove', function (e) { + if (paint) draw(drawingContext, e.offsetX, e.offsetY); + }); + + drawingCanvas.addEventListener('pointerup', function () { + paint = false; + }); + + drawingCanvas.addEventListener('pointerleave', function () { + paint = false; + }); +} + +function draw(drawContext, x, y) { + drawContext.moveTo(drawStartPos.x, drawStartPos.y); + drawContext.strokeStyle = '#000000'; + drawContext.lineTo(x, y); + drawContext.stroke(); + // reset drawing start position to current position. + drawStartPos.set(x, y); + // need to flag the map as needing updating. + material.map.needsUpdate = true; +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + mesh.rotation.x += 0.01; + mesh.rotation.y += 0.01; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_texture_filters.ts b/examples-testing/examples/webgl_materials_texture_filters.ts new file mode 100644 index 000000000..178c2ce49 --- /dev/null +++ b/examples-testing/examples/webgl_materials_texture_filters.ts @@ -0,0 +1,164 @@ +import * as THREE from 'three'; + +const SCREEN_WIDTH = window.innerWidth; +const SCREEN_HEIGHT = window.innerHeight; + +let container; + +let camera, scene, scene2, renderer; + +let mouseX = 0, + mouseY = 0; + +const windowHalfX = window.innerWidth / 2; +const windowHalfY = window.innerHeight / 2; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(35, SCREEN_WIDTH / SCREEN_HEIGHT, 1, 5000); + camera.position.z = 1500; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x000000); + scene.fog = new THREE.Fog(0x000000, 1500, 4000); + + scene2 = new THREE.Scene(); + scene2.background = new THREE.Color(0x000000); + scene2.fog = new THREE.Fog(0x000000, 1500, 4000); + + // GROUND + + const imageCanvas = document.createElement('canvas'); + const context = imageCanvas.getContext('2d'); + + imageCanvas.width = imageCanvas.height = 128; + + context.fillStyle = '#444'; + context.fillRect(0, 0, 128, 128); + + context.fillStyle = '#fff'; + context.fillRect(0, 0, 64, 64); + context.fillRect(64, 64, 64, 64); + + const textureCanvas = new THREE.CanvasTexture(imageCanvas); + textureCanvas.colorSpace = THREE.SRGBColorSpace; + textureCanvas.repeat.set(1000, 1000); + textureCanvas.wrapS = THREE.RepeatWrapping; + textureCanvas.wrapT = THREE.RepeatWrapping; + + const textureCanvas2 = textureCanvas.clone(); + textureCanvas2.magFilter = THREE.NearestFilter; + textureCanvas2.minFilter = THREE.NearestFilter; + + const materialCanvas = new THREE.MeshBasicMaterial({ map: textureCanvas }); + const materialCanvas2 = new THREE.MeshBasicMaterial({ color: 0xffccaa, map: textureCanvas2 }); + + const geometry = new THREE.PlaneGeometry(100, 100); + + const meshCanvas = new THREE.Mesh(geometry, materialCanvas); + meshCanvas.rotation.x = -Math.PI / 2; + meshCanvas.scale.set(1000, 1000, 1000); + + const meshCanvas2 = new THREE.Mesh(geometry, materialCanvas2); + meshCanvas2.rotation.x = -Math.PI / 2; + meshCanvas2.scale.set(1000, 1000, 1000); + + // PAINTING + + const callbackPainting = function () { + const image = texturePainting.image; + + texturePainting2.image = image; + texturePainting2.needsUpdate = true; + + scene.add(meshCanvas); + scene2.add(meshCanvas2); + + const geometry = new THREE.PlaneGeometry(100, 100); + const mesh = new THREE.Mesh(geometry, materialPainting); + const mesh2 = new THREE.Mesh(geometry, materialPainting2); + + addPainting(scene, mesh); + addPainting(scene2, mesh2); + + function addPainting(zscene, zmesh) { + zmesh.scale.x = image.width / 100; + zmesh.scale.y = image.height / 100; + + zscene.add(zmesh); + + const meshFrame = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ color: 0x000000 })); + meshFrame.position.z = -10.0; + meshFrame.scale.x = (1.1 * image.width) / 100; + meshFrame.scale.y = (1.1 * image.height) / 100; + zscene.add(meshFrame); + + const meshShadow = new THREE.Mesh( + geometry, + new THREE.MeshBasicMaterial({ color: 0x000000, opacity: 0.75, transparent: true }), + ); + meshShadow.position.y = (-1.1 * image.height) / 2; + meshShadow.position.z = (-1.1 * image.height) / 2; + meshShadow.rotation.x = -Math.PI / 2; + meshShadow.scale.x = (1.1 * image.width) / 100; + meshShadow.scale.y = (1.1 * image.height) / 100; + zscene.add(meshShadow); + + const floorHeight = (-1.117 * image.height) / 2; + meshCanvas.position.y = meshCanvas2.position.y = floorHeight; + } + }; + + const texturePainting = new THREE.TextureLoader().load( + 'textures/758px-Canestra_di_frutta_(Caravaggio).jpg', + callbackPainting, + ); + const texturePainting2 = new THREE.Texture(); + + const materialPainting = new THREE.MeshBasicMaterial({ color: 0xffffff, map: texturePainting }); + const materialPainting2 = new THREE.MeshBasicMaterial({ color: 0xffccaa, map: texturePainting2 }); + + texturePainting.colorSpace = THREE.SRGBColorSpace; + texturePainting2.colorSpace = THREE.SRGBColorSpace; + texturePainting2.minFilter = texturePainting2.magFilter = THREE.NearestFilter; + texturePainting.minFilter = texturePainting.magFilter = THREE.LinearFilter; + texturePainting.mapping = THREE.UVMapping; + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + renderer.setAnimationLoop(animate); + renderer.autoClear = false; + + renderer.domElement.style.position = 'relative'; + container.appendChild(renderer.domElement); + + document.addEventListener('mousemove', onDocumentMouseMove); +} + +function onDocumentMouseMove(event) { + mouseX = event.clientX - windowHalfX; + mouseY = event.clientY - windowHalfY; +} + +function animate() { + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-(mouseY - 200) - camera.position.y) * 0.05; + + camera.lookAt(scene.position); + + renderer.clear(); + renderer.setScissorTest(true); + + renderer.setScissor(0, 0, SCREEN_WIDTH / 2 - 2, SCREEN_HEIGHT); + renderer.render(scene, camera); + + renderer.setScissor(SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2 - 2, SCREEN_HEIGHT); + renderer.render(scene2, camera); + + renderer.setScissorTest(false); +} diff --git a/examples-testing/examples/webgl_materials_texture_manualmipmap.ts b/examples-testing/examples/webgl_materials_texture_manualmipmap.ts new file mode 100644 index 000000000..24bd4eb9f --- /dev/null +++ b/examples-testing/examples/webgl_materials_texture_manualmipmap.ts @@ -0,0 +1,175 @@ +import * as THREE from 'three'; + +const SCREEN_WIDTH = window.innerWidth; +const SCREEN_HEIGHT = window.innerHeight; + +let container; + +let camera, scene1, scene2, renderer; + +let mouseX = 0, + mouseY = 0; + +const windowHalfX = window.innerWidth / 2; +const windowHalfY = window.innerHeight / 2; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(35, SCREEN_WIDTH / SCREEN_HEIGHT, 1, 5000); + camera.position.z = 1500; + + scene1 = new THREE.Scene(); + scene1.background = new THREE.Color(0x000000); + scene1.fog = new THREE.Fog(0x000000, 1500, 4000); + + scene2 = new THREE.Scene(); + scene2.background = new THREE.Color(0x000000); + scene2.fog = new THREE.Fog(0x000000, 1500, 4000); + + // GROUND + + function mipmap(size, color) { + const imageCanvas = document.createElement('canvas'); + const context = imageCanvas.getContext('2d'); + + imageCanvas.width = imageCanvas.height = size; + + context.fillStyle = '#444'; + context.fillRect(0, 0, size, size); + + context.fillStyle = color; + context.fillRect(0, 0, size / 2, size / 2); + context.fillRect(size / 2, size / 2, size / 2, size / 2); + return imageCanvas; + } + + const canvas = mipmap(128, '#f00'); + const textureCanvas1 = new THREE.CanvasTexture(canvas); + textureCanvas1.mipmaps[0] = canvas; + textureCanvas1.mipmaps[1] = mipmap(64, '#0f0'); + textureCanvas1.mipmaps[2] = mipmap(32, '#00f'); + textureCanvas1.mipmaps[3] = mipmap(16, '#400'); + textureCanvas1.mipmaps[4] = mipmap(8, '#040'); + textureCanvas1.mipmaps[5] = mipmap(4, '#004'); + textureCanvas1.mipmaps[6] = mipmap(2, '#044'); + textureCanvas1.mipmaps[7] = mipmap(1, '#404'); + textureCanvas1.colorSpace = THREE.SRGBColorSpace; + textureCanvas1.repeat.set(1000, 1000); + textureCanvas1.wrapS = THREE.RepeatWrapping; + textureCanvas1.wrapT = THREE.RepeatWrapping; + + const textureCanvas2 = textureCanvas1.clone(); + textureCanvas2.magFilter = THREE.NearestFilter; + textureCanvas2.minFilter = THREE.NearestMipmapNearestFilter; + + const materialCanvas1 = new THREE.MeshBasicMaterial({ map: textureCanvas1 }); + const materialCanvas2 = new THREE.MeshBasicMaterial({ color: 0xffccaa, map: textureCanvas2 }); + + const geometry = new THREE.PlaneGeometry(100, 100); + + const meshCanvas1 = new THREE.Mesh(geometry, materialCanvas1); + meshCanvas1.rotation.x = -Math.PI / 2; + meshCanvas1.scale.set(1000, 1000, 1000); + + const meshCanvas2 = new THREE.Mesh(geometry, materialCanvas2); + meshCanvas2.rotation.x = -Math.PI / 2; + meshCanvas2.scale.set(1000, 1000, 1000); + + // PAINTING + + const callbackPainting = function () { + const image = texturePainting1.image; + + texturePainting2.image = image; + texturePainting2.needsUpdate = true; + + scene1.add(meshCanvas1); + scene2.add(meshCanvas2); + + const geometry = new THREE.PlaneGeometry(100, 100); + const mesh1 = new THREE.Mesh(geometry, materialPainting1); + const mesh2 = new THREE.Mesh(geometry, materialPainting2); + + addPainting(scene1, mesh1); + addPainting(scene2, mesh2); + + function addPainting(zscene, zmesh) { + zmesh.scale.x = image.width / 100; + zmesh.scale.y = image.height / 100; + + zscene.add(zmesh); + + const meshFrame = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ color: 0x000000 })); + meshFrame.position.z = -10.0; + meshFrame.scale.x = (1.1 * image.width) / 100; + meshFrame.scale.y = (1.1 * image.height) / 100; + zscene.add(meshFrame); + + const meshShadow = new THREE.Mesh( + geometry, + new THREE.MeshBasicMaterial({ color: 0x000000, opacity: 0.75, transparent: true }), + ); + meshShadow.position.y = (-1.1 * image.height) / 2; + meshShadow.position.z = (-1.1 * image.height) / 2; + meshShadow.rotation.x = -Math.PI / 2; + meshShadow.scale.x = (1.1 * image.width) / 100; + meshShadow.scale.y = (1.1 * image.height) / 100; + zscene.add(meshShadow); + + const floorHeight = (-1.117 * image.height) / 2; + meshCanvas1.position.y = meshCanvas2.position.y = floorHeight; + } + }; + + const texturePainting1 = new THREE.TextureLoader().load( + 'textures/758px-Canestra_di_frutta_(Caravaggio).jpg', + callbackPainting, + ); + const texturePainting2 = new THREE.Texture(); + const materialPainting1 = new THREE.MeshBasicMaterial({ color: 0xffffff, map: texturePainting1 }); + const materialPainting2 = new THREE.MeshBasicMaterial({ color: 0xffccaa, map: texturePainting2 }); + + texturePainting1.colorSpace = THREE.SRGBColorSpace; + texturePainting2.colorSpace = THREE.SRGBColorSpace; + texturePainting2.minFilter = texturePainting2.magFilter = THREE.NearestFilter; + texturePainting1.minFilter = texturePainting1.magFilter = THREE.LinearFilter; + texturePainting1.mapping = THREE.UVMapping; + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + renderer.setAnimationLoop(animate); + renderer.autoClear = false; + + renderer.domElement.style.position = 'relative'; + container.appendChild(renderer.domElement); + + document.addEventListener('mousemove', onDocumentMouseMove); +} + +function onDocumentMouseMove(event) { + mouseX = event.clientX - windowHalfX; + mouseY = event.clientY - windowHalfY; +} + +function animate() { + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-(mouseY - 200) - camera.position.y) * 0.05; + + camera.lookAt(scene1.position); + + renderer.clear(); + renderer.setScissorTest(true); + + renderer.setScissor(0, 0, SCREEN_WIDTH / 2 - 2, SCREEN_HEIGHT); + renderer.render(scene1, camera); + + renderer.setScissor(SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2 - 2, SCREEN_HEIGHT); + renderer.render(scene2, camera); + + renderer.setScissorTest(false); +} diff --git a/examples-testing/examples/webgl_materials_texture_partialupdate.ts b/examples-testing/examples/webgl_materials_texture_partialupdate.ts new file mode 100644 index 000000000..5adfc8e69 --- /dev/null +++ b/examples-testing/examples/webgl_materials_texture_partialupdate.ts @@ -0,0 +1,100 @@ +import * as THREE from 'three'; + +let camera, scene, renderer, clock, dataTexture, diffuseMap; + +let last = 0; +const position = new THREE.Vector2(); +const color = new THREE.Color(); + +init(); + +async function init() { + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10); + camera.position.z = 2; + + scene = new THREE.Scene(); + + clock = new THREE.Clock(); + + const loader = new THREE.TextureLoader(); + diffuseMap = await loader.loadAsync('textures/floors/FloorsCheckerboard_S_Diffuse.jpg'); + diffuseMap.colorSpace = THREE.SRGBColorSpace; + diffuseMap.minFilter = THREE.LinearFilter; + diffuseMap.generateMipmaps = false; + + const geometry = new THREE.PlaneGeometry(2, 2); + const material = new THREE.MeshBasicMaterial({ map: diffuseMap }); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + const width = 32; + const height = 32; + + const data = new Uint8Array(width * height * 4); + dataTexture = new THREE.DataTexture(data, width, height); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const elapsedTime = clock.getElapsedTime(); + + if (elapsedTime - last > 0.1) { + last = elapsedTime; + + position.x = 32 * THREE.MathUtils.randInt(1, 16) - 32; + position.y = 32 * THREE.MathUtils.randInt(1, 16) - 32; + + // generate new color data + + updateDataTexture(dataTexture); + + // perform copy from src to dest texture to a random position + + renderer.copyTextureToTexture(dataTexture, diffuseMap, null, position); + } + + renderer.render(scene, camera); +} + +function updateDataTexture(texture) { + const size = texture.image.width * texture.image.height; + const data = texture.image.data; + + // generate a random color and update texture data + + color.setHex(Math.random() * 0xffffff); + + const r = Math.floor(color.r * 255); + const g = Math.floor(color.g * 255); + const b = Math.floor(color.b * 255); + + for (let i = 0; i < size; i++) { + const stride = i * 4; + + data[stride] = r; + data[stride + 1] = g; + data[stride + 2] = b; + data[stride + 3] = 1; + } +} diff --git a/examples-testing/examples/webgl_materials_texture_rotation.ts b/examples-testing/examples/webgl_materials_texture_rotation.ts new file mode 100644 index 000000000..2666d09d6 --- /dev/null +++ b/examples-testing/examples/webgl_materials_texture_rotation.ts @@ -0,0 +1,113 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let mesh, renderer, scene, camera; + +let gui; + +const API = { + offsetX: 0, + offsetY: 0, + repeatX: 0.25, + repeatY: 0.25, + rotation: Math.PI / 4, // positive is counterclockwise + centerX: 0.5, + centerY: 0.5, +}; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(10, 15, 25); + scene.add(camera); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); + controls.minDistance = 20; + controls.maxDistance = 50; + controls.maxPolarAngle = Math.PI / 2; + + const geometry = new THREE.BoxGeometry(10, 10, 10); + + new THREE.TextureLoader().load('textures/uv_grid_opengl.jpg', function (texture) { + texture.wrapS = texture.wrapT = THREE.RepeatWrapping; + texture.anisotropy = renderer.capabilities.getMaxAnisotropy(); + texture.colorSpace = THREE.SRGBColorSpace; + + //texture.matrixAutoUpdate = false; // default true; set to false to update texture.matrix manually + + const material = new THREE.MeshBasicMaterial({ map: texture }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + updateUvTransform(); + + initGui(); + + render(); + }); + + window.addEventListener('resize', onWindowResize); +} + +function render() { + renderer.render(scene, camera); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function updateUvTransform() { + const texture = mesh.material.map; + + if (texture.matrixAutoUpdate === true) { + texture.offset.set(API.offsetX, API.offsetY); + texture.repeat.set(API.repeatX, API.repeatY); + texture.center.set(API.centerX, API.centerY); + texture.rotation = API.rotation; // rotation is around center + } else { + // setting the matrix uv transform directly + //texture.matrix.setUvTransform( API.offsetX, API.offsetY, API.repeatX, API.repeatY, API.rotation, API.centerX, API.centerY ); + + // another way... + texture.matrix + .identity() + .translate(-API.centerX, -API.centerY) + .rotate(API.rotation) // I don't understand how rotation can preceed scale, but it seems to be required... + .scale(API.repeatX, API.repeatY) + .translate(API.centerX, API.centerY) + .translate(API.offsetX, API.offsetY); + } + + render(); +} + +function initGui() { + gui = new GUI(); + + gui.add(API, 'offsetX', 0.0, 1.0).name('offset.x').onChange(updateUvTransform); + gui.add(API, 'offsetY', 0.0, 1.0).name('offset.y').onChange(updateUvTransform); + gui.add(API, 'repeatX', 0.25, 2.0).name('repeat.x').onChange(updateUvTransform); + gui.add(API, 'repeatY', 0.25, 2.0).name('repeat.y').onChange(updateUvTransform); + gui.add(API, 'rotation', -2.0, 2.0).name('rotation').onChange(updateUvTransform); + gui.add(API, 'centerX', 0.0, 1.0).name('center.x').onChange(updateUvTransform); + gui.add(API, 'centerY', 0.0, 1.0).name('center.y').onChange(updateUvTransform); +} diff --git a/examples-testing/examples/webgl_materials_toon.ts b/examples-testing/examples/webgl_materials_toon.ts new file mode 100644 index 000000000..03db286ad --- /dev/null +++ b/examples-testing/examples/webgl_materials_toon.ts @@ -0,0 +1,152 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { OutlineEffect } from 'three/addons/effects/OutlineEffect.js'; +import { FontLoader } from 'three/addons/loaders/FontLoader.js'; +import { TextGeometry } from 'three/addons/geometries/TextGeometry.js'; + +let container, stats; + +let camera, scene, renderer, effect; +let particleLight; + +const loader = new FontLoader(); +loader.load('fonts/gentilis_regular.typeface.json', function (font) { + init(font); +}); + +function init(font) { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 2500); + camera.position.set(0.0, 400, 400 * 3.5); + + // + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x444488); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // Materials + + const cubeWidth = 400; + const numberOfSphersPerSide = 5; + const sphereRadius = (cubeWidth / numberOfSphersPerSide) * 0.8 * 0.5; + const stepSize = 1.0 / numberOfSphersPerSide; + + const geometry = new THREE.SphereGeometry(sphereRadius, 32, 16); + + for (let alpha = 0, alphaIndex = 0; alpha <= 1.0; alpha += stepSize, alphaIndex++) { + const colors = new Uint8Array(alphaIndex + 2); + + for (let c = 0; c <= colors.length; c++) { + colors[c] = (c / colors.length) * 256; + } + + const gradientMap = new THREE.DataTexture(colors, colors.length, 1, THREE.RedFormat); + gradientMap.needsUpdate = true; + + for (let beta = 0; beta <= 1.0; beta += stepSize) { + for (let gamma = 0; gamma <= 1.0; gamma += stepSize) { + // basic monochromatic energy preservation + const diffuseColor = new THREE.Color() + .setHSL(alpha, 0.5, gamma * 0.5 + 0.1) + .multiplyScalar(1 - beta * 0.2); + + const material = new THREE.MeshToonMaterial({ + color: diffuseColor, + gradientMap: gradientMap, + }); + + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.x = alpha * 400 - 200; + mesh.position.y = beta * 400 - 200; + mesh.position.z = gamma * 400 - 200; + + scene.add(mesh); + } + } + } + + function addLabel(name, location) { + const textGeo = new TextGeometry(name, { + font: font, + + size: 20, + depth: 1, + curveSegments: 1, + }); + + const textMaterial = new THREE.MeshBasicMaterial(); + const textMesh = new THREE.Mesh(textGeo, textMaterial); + textMesh.position.copy(location); + scene.add(textMesh); + } + + addLabel('-gradientMap', new THREE.Vector3(-350, 0, 0)); + addLabel('+gradientMap', new THREE.Vector3(350, 0, 0)); + + addLabel('-diffuse', new THREE.Vector3(0, 0, -300)); + addLabel('+diffuse', new THREE.Vector3(0, 0, 300)); + + particleLight = new THREE.Mesh(new THREE.SphereGeometry(4, 8, 8), new THREE.MeshBasicMaterial({ color: 0xffffff })); + scene.add(particleLight); + + // Lights + + scene.add(new THREE.AmbientLight(0xc1c1c1, 3)); + + const pointLight = new THREE.PointLight(0xffffff, 2, 800, 0); + particleLight.add(pointLight); + + // + + effect = new OutlineEffect(renderer); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 200; + controls.maxDistance = 2000; + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + stats.begin(); + render(); + stats.end(); +} + +function render() { + const timer = Date.now() * 0.00025; + + particleLight.position.x = Math.sin(timer * 7) * 300; + particleLight.position.y = Math.cos(timer * 5) * 400; + particleLight.position.z = Math.cos(timer * 3) * 300; + + effect.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_video.ts b/examples-testing/examples/webgl_materials_video.ts new file mode 100644 index 000000000..4f0d26a18 --- /dev/null +++ b/examples-testing/examples/webgl_materials_video.ts @@ -0,0 +1,208 @@ +import * as THREE from 'three'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { BloomPass } from 'three/addons/postprocessing/BloomPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +let container; + +let camera, scene, renderer; + +let video, texture, material, mesh; + +let composer; + +let mouseX = 0; +let mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +let cube_count; + +const meshes = [], + materials = [], + xgrid = 20, + ygrid = 10; + +const startButton = document.getElementById('startButton'); +startButton.addEventListener('click', function () { + init(); +}); + +function init() { + const overlay = document.getElementById('overlay'); + overlay.remove(); + + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 500; + + scene = new THREE.Scene(); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0.5, 1, 1).normalize(); + scene.add(light); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + video = document.getElementById('video'); + video.play(); + video.addEventListener('play', function () { + this.currentTime = 3; + }); + + texture = new THREE.VideoTexture(video); + texture.colorSpace = THREE.SRGBColorSpace; + + // + + let i, j, ox, oy, geometry; + + const ux = 1 / xgrid; + const uy = 1 / ygrid; + + const xsize = 480 / xgrid; + const ysize = 204 / ygrid; + + const parameters = { color: 0xffffff, map: texture }; + + cube_count = 0; + + for (i = 0; i < xgrid; i++) { + for (j = 0; j < ygrid; j++) { + ox = i; + oy = j; + + geometry = new THREE.BoxGeometry(xsize, ysize, xsize); + + change_uvs(geometry, ux, uy, ox, oy); + + materials[cube_count] = new THREE.MeshLambertMaterial(parameters); + + material = materials[cube_count]; + + material.hue = i / xgrid; + material.saturation = 1 - j / ygrid; + + material.color.setHSL(material.hue, material.saturation, 0.5); + + mesh = new THREE.Mesh(geometry, material); + + mesh.position.x = (i - xgrid / 2) * xsize; + mesh.position.y = (j - ygrid / 2) * ysize; + mesh.position.z = 0; + + mesh.scale.x = mesh.scale.y = mesh.scale.z = 1; + + scene.add(mesh); + + mesh.dx = 0.001 * (0.5 - Math.random()); + mesh.dy = 0.001 * (0.5 - Math.random()); + + meshes[cube_count] = mesh; + + cube_count += 1; + } + } + + renderer.autoClear = false; + + document.addEventListener('mousemove', onDocumentMouseMove); + + // postprocessing + + const renderPass = new RenderPass(scene, camera); + const bloomPass = new BloomPass(1.3); + const outputPass = new OutputPass(); + + composer = new EffectComposer(renderer); + + composer.addPass(renderPass); + composer.addPass(bloomPass); + composer.addPass(outputPass); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); +} + +function change_uvs(geometry, unitx, unity, offsetx, offsety) { + const uvs = geometry.attributes.uv.array; + + for (let i = 0; i < uvs.length; i += 2) { + uvs[i] = (uvs[i] + offsetx) * unitx; + uvs[i + 1] = (uvs[i + 1] + offsety) * unity; + } +} + +function onDocumentMouseMove(event) { + mouseX = event.clientX - windowHalfX; + mouseY = (event.clientY - windowHalfY) * 0.3; +} + +// + +let h, + counter = 1; + +function animate() { + const time = Date.now() * 0.00005; + + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-mouseY - camera.position.y) * 0.05; + + camera.lookAt(scene.position); + + for (let i = 0; i < cube_count; i++) { + material = materials[i]; + + h = ((360 * (material.hue + time)) % 360) / 360; + material.color.setHSL(h, material.saturation, 0.5); + } + + if (counter % 1000 > 200) { + for (let i = 0; i < cube_count; i++) { + mesh = meshes[i]; + + mesh.rotation.x += 10 * mesh.dx; + mesh.rotation.y += 10 * mesh.dy; + + mesh.position.x -= 150 * mesh.dx; + mesh.position.y += 150 * mesh.dy; + mesh.position.z += 300 * mesh.dx; + } + } + + if (counter % 1000 === 0) { + for (let i = 0; i < cube_count; i++) { + mesh = meshes[i]; + + mesh.dx *= -1; + mesh.dy *= -1; + } + } + + counter++; + + renderer.clear(); + composer.render(); +} diff --git a/examples-testing/examples/webgl_materials_video_webcam.ts b/examples-testing/examples/webgl_materials_video_webcam.ts new file mode 100644 index 000000000..cf6f8d50c --- /dev/null +++ b/examples-testing/examples/webgl_materials_video_webcam.ts @@ -0,0 +1,79 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer, video; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.z = 0.01; + + scene = new THREE.Scene(); + + video = document.getElementById('video'); + + const texture = new THREE.VideoTexture(video); + texture.colorSpace = THREE.SRGBColorSpace; + + const geometry = new THREE.PlaneGeometry(16, 9); + geometry.scale(0.5, 0.5, 0.5); + const material = new THREE.MeshBasicMaterial({ map: texture }); + + const count = 128; + const radius = 32; + + for (let i = 1, l = count; i <= l; i++) { + const phi = Math.acos(-1 + (2 * i) / l); + const theta = Math.sqrt(l * Math.PI) * phi; + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.setFromSphericalCoords(radius, phi, theta); + mesh.lookAt(camera.position); + scene.add(mesh); + } + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableZoom = false; + controls.enablePan = false; + + window.addEventListener('resize', onWindowResize); + + // + + if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + const constraints = { video: { width: 1280, height: 720, facingMode: 'user' } }; + + navigator.mediaDevices + .getUserMedia(constraints) + .then(function (stream) { + // apply the stream to the video element used in the texture + + video.srcObject = stream; + video.play(); + }) + .catch(function (error) { + console.error('Unable to access the camera/webcam.', error); + }); + } else { + console.error('MediaDevices interface not available.'); + } +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_materials_wireframe.ts b/examples-testing/examples/webgl_materials_wireframe.ts new file mode 100644 index 000000000..8adbd71d6 --- /dev/null +++ b/examples-testing/examples/webgl_materials_wireframe.ts @@ -0,0 +1,107 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +const API = { + thickness: 1, +}; + +let renderer, scene, camera, mesh2; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 500); + camera.position.z = 200; + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use if there is no animation loop + controls.enablePan = false; + controls.enableZoom = false; + + new THREE.BufferGeometryLoader().load('models/json/WaltHeadLo_buffergeometry.json', function (geometry) { + geometry.deleteAttribute('normal'); + geometry.deleteAttribute('uv'); + + setupAttributes(geometry); + + // left + + const material1 = new THREE.MeshBasicMaterial({ + color: 0xe0e0ff, + wireframe: true, + }); + + const mesh1 = new THREE.Mesh(geometry, material1); + mesh1.position.set(-40, 0, 0); + + scene.add(mesh1); + + // right + + const material2 = new THREE.ShaderMaterial({ + uniforms: { thickness: { value: API.thickness } }, + vertexShader: document.getElementById('vertexShader').textContent, + fragmentShader: document.getElementById('fragmentShader').textContent, + side: THREE.DoubleSide, + alphaToCoverage: true, // only works when WebGLRenderer's "antialias" is set to "true" + }); + + mesh2 = new THREE.Mesh(geometry, material2); + mesh2.position.set(40, 0, 0); + + scene.add(mesh2); + + // + + render(); + }); + + // + + const gui = new GUI(); + + gui.add(API, 'thickness', 0, 4).onChange(function () { + mesh2.material.uniforms.thickness.value = API.thickness; + render(); + }); + + gui.open(); + + // + + window.addEventListener('resize', onWindowResize); +} + +function setupAttributes(geometry) { + const vectors = [new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 1)]; + + const position = geometry.attributes.position; + const centers = new Float32Array(position.count * 3); + + for (let i = 0, l = position.count; i < l; i++) { + vectors[i % 3].toArray(centers, i * 3); + } + + geometry.setAttribute('center', new THREE.BufferAttribute(centers, 3)); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_math_obb.ts b/examples-testing/examples/webgl_math_obb.ts new file mode 100644 index 000000000..48480d10b --- /dev/null +++ b/examples-testing/examples/webgl_math_obb.ts @@ -0,0 +1,189 @@ +import * as THREE from 'three'; + +import { OBB } from 'three/addons/math/OBB.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let camera, scene, renderer, clock, controls, stats, raycaster, hitbox; + +const objects = [], + mouse = new THREE.Vector2(); + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 0, 75); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + + clock = new THREE.Clock(); + + raycaster = new THREE.Raycaster(); + + const hemiLight = new THREE.HemisphereLight(0xffffff, 0x222222, 4); + hemiLight.position.set(1, 1, 1); + scene.add(hemiLight); + + const size = new THREE.Vector3(10, 5, 6); + const geometry = new THREE.BoxGeometry(size.x, size.y, size.z); + + // setup OBB on geometry level (doing this manually for now) + + geometry.userData.obb = new OBB(); + geometry.userData.obb.halfSize.copy(size).multiplyScalar(0.5); + + for (let i = 0; i < 100; i++) { + const object = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ color: 0x00ff00 })); + object.matrixAutoUpdate = false; + + object.position.x = Math.random() * 80 - 40; + object.position.y = Math.random() * 80 - 40; + object.position.z = Math.random() * 80 - 40; + + object.rotation.x = Math.random() * 2 * Math.PI; + object.rotation.y = Math.random() * 2 * Math.PI; + object.rotation.z = Math.random() * 2 * Math.PI; + + object.scale.x = Math.random() + 0.5; + object.scale.y = Math.random() + 0.5; + object.scale.z = Math.random() + 0.5; + + scene.add(object); + + // bounding volume on object level (this will reflect the current world transform) + + object.userData.obb = new OBB(); + + objects.push(object); + } + + // + + hitbox = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ color: 0x000000, wireframe: true })); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); + + document.addEventListener('click', onClick); +} + +function onClick(event) { + event.preventDefault(); + + mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; + + raycaster.setFromCamera(mouse, camera); + + const intersectionPoint = new THREE.Vector3(); + const intersections = []; + + for (let i = 0, il = objects.length; i < il; i++) { + const object = objects[i]; + const obb = object.userData.obb; + + const ray = raycaster.ray; + + if (obb.intersectRay(ray, intersectionPoint) !== null) { + const distance = ray.origin.distanceTo(intersectionPoint); + intersections.push({ distance: distance, object: object }); + } + } + + if (intersections.length > 0) { + // determine closest intersection and highlight the respective 3D object + + intersections.sort(sortIntersections); + + intersections[0].object.add(hitbox); + } else { + const parent = hitbox.parent; + + if (parent) parent.remove(hitbox); + } +} + +function sortIntersections(a, b) { + return a.distance - b.distance; +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + controls.update(); + + // transform cubes + + const delta = clock.getDelta(); + + for (let i = 0, il = objects.length; i < il; i++) { + const object = objects[i]; + + object.rotation.x += delta * Math.PI * 0.2; + object.rotation.y += delta * Math.PI * 0.1; + + object.updateMatrix(); + object.updateMatrixWorld(); + + // update OBB + + object.userData.obb.copy(object.geometry.userData.obb); + object.userData.obb.applyMatrix4(object.matrixWorld); + + // reset + + object.material.color.setHex(0x00ff00); + } + + // collision detection + + for (let i = 0, il = objects.length; i < il; i++) { + const object = objects[i]; + const obb = object.userData.obb; + + for (let j = i + 1, jl = objects.length; j < jl; j++) { + const objectToTest = objects[j]; + const obbToTest = objectToTest.userData.obb; + + // now perform intersection test + + if (obb.intersectsOBB(obbToTest) === true) { + object.material.color.setHex(0xff0000); + objectToTest.material.color.setHex(0xff0000); + } + } + } + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_math_orientation_transform.ts b/examples-testing/examples/webgl_math_orientation_transform.ts new file mode 100644 index 000000000..99be247d8 --- /dev/null +++ b/examples-testing/examples/webgl_math_orientation_transform.ts @@ -0,0 +1,95 @@ +import * as THREE from 'three'; + +let camera, scene, renderer, mesh, target; + +const spherical = new THREE.Spherical(); +const rotationMatrix = new THREE.Matrix4(); +const targetQuaternion = new THREE.Quaternion(); +const clock = new THREE.Clock(); +const speed = 2; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10); + camera.position.z = 5; + + scene = new THREE.Scene(); + + const geometry = new THREE.ConeGeometry(0.1, 0.5, 8); + geometry.rotateX(Math.PI * 0.5); + const material = new THREE.MeshNormalMaterial(); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + const targetGeometry = new THREE.SphereGeometry(0.05); + const targetMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); + target = new THREE.Mesh(targetGeometry, targetMaterial); + scene.add(target); + + // + + const sphereGeometry = new THREE.SphereGeometry(2, 32, 32); + const sphereMaterial = new THREE.MeshBasicMaterial({ + color: 0xcccccc, + wireframe: true, + transparent: true, + opacity: 0.3, + }); + const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); + scene.add(sphere); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + window.addEventListener('resize', onWindowResize); + + // + + generateTarget(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const delta = clock.getDelta(); + + if (!mesh.quaternion.equals(targetQuaternion)) { + const step = speed * delta; + mesh.quaternion.rotateTowards(targetQuaternion, step); + } + + renderer.render(scene, camera); +} + +function generateTarget() { + // generate a random point on a sphere + + spherical.theta = Math.random() * Math.PI * 2; + spherical.phi = Math.acos(2 * Math.random() - 1); + spherical.radius = 2; + + target.position.setFromSpherical(spherical); + + // compute target rotation + + rotationMatrix.lookAt(target.position, mesh.position, mesh.up); + targetQuaternion.setFromRotationMatrix(rotationMatrix); + + setTimeout(generateTarget, 2000); +} diff --git a/examples-testing/examples/webgl_mesh_batch.ts b/examples-testing/examples/webgl_mesh_batch.ts new file mode 100644 index 000000000..f93e5fb85 --- /dev/null +++ b/examples-testing/examples/webgl_mesh_batch.ts @@ -0,0 +1,305 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { radixSort } from 'three/addons/utils/SortUtils.js'; + +let stats, gui, guiStatsEl; +let camera, controls, scene, renderer; +let geometries, mesh, material; +const ids = []; +const matrix = new THREE.Matrix4(); + +// + +const position = new THREE.Vector3(); +const rotation = new THREE.Euler(); +const quaternion = new THREE.Quaternion(); +const scale = new THREE.Vector3(); + +// + +const MAX_GEOMETRY_COUNT = 20000; + +const Method = { + BATCHED: 'BATCHED', + NAIVE: 'NAIVE', +}; + +const api = { + method: Method.BATCHED, + count: 256, + dynamic: 16, + + sortObjects: true, + perObjectFrustumCulled: true, + opacity: 1, + useCustomSort: true, +}; + +init(); +initGeometries(); +initMesh(); + +// + +function randomizeMatrix(matrix) { + position.x = Math.random() * 40 - 20; + position.y = Math.random() * 40 - 20; + position.z = Math.random() * 40 - 20; + + rotation.x = Math.random() * 2 * Math.PI; + rotation.y = Math.random() * 2 * Math.PI; + rotation.z = Math.random() * 2 * Math.PI; + + quaternion.setFromEuler(rotation); + + scale.x = scale.y = scale.z = 0.5 + Math.random() * 0.5; + + return matrix.compose(position, quaternion, scale); +} + +function randomizeRotationSpeed(rotation) { + rotation.x = Math.random() * 0.01; + rotation.y = Math.random() * 0.01; + rotation.z = Math.random() * 0.01; + return rotation; +} + +function initGeometries() { + geometries = [ + new THREE.ConeGeometry(1.0, 2.0), + new THREE.BoxGeometry(2.0, 2.0, 2.0), + new THREE.SphereGeometry(1.0, 16, 8), + ]; +} + +function createMaterial() { + if (!material) { + material = new THREE.MeshNormalMaterial(); + } + + return material; +} + +function cleanup() { + if (mesh) { + mesh.parent.remove(mesh); + + if (mesh.dispose) { + mesh.dispose(); + } + } +} + +function initMesh() { + cleanup(); + + if (api.method === Method.BATCHED) { + initBatchedMesh(); + } else { + initRegularMesh(); + } +} + +function initRegularMesh() { + mesh = new THREE.Group(); + const material = createMaterial(); + + for (let i = 0; i < api.count; i++) { + const child = new THREE.Mesh(geometries[i % geometries.length], material); + randomizeMatrix(child.matrix); + child.matrix.decompose(child.position, child.quaternion, child.scale); + child.userData.rotationSpeed = randomizeRotationSpeed(new THREE.Euler()); + mesh.add(child); + } + + scene.add(mesh); +} + +function initBatchedMesh() { + const geometryCount = api.count; + const vertexCount = geometries.length * 512; + const indexCount = geometries.length * 1024; + + const euler = new THREE.Euler(); + const matrix = new THREE.Matrix4(); + mesh = new THREE.BatchedMesh(geometryCount, vertexCount, indexCount, createMaterial()); + mesh.userData.rotationSpeeds = []; + + // disable full-object frustum culling since all of the objects can be dynamic. + mesh.frustumCulled = false; + + ids.length = 0; + + const geometryIds = [ + mesh.addGeometry(geometries[0]), + mesh.addGeometry(geometries[1]), + mesh.addGeometry(geometries[2]), + ]; + + for (let i = 0; i < api.count; i++) { + const id = mesh.addInstance(geometryIds[i % geometryIds.length]); + mesh.setMatrixAt(id, randomizeMatrix(matrix)); + + const rotationMatrix = new THREE.Matrix4(); + rotationMatrix.makeRotationFromEuler(randomizeRotationSpeed(euler)); + mesh.userData.rotationSpeeds.push(rotationMatrix); + + ids.push(id); + } + + scene.add(mesh); +} + +function init() { + const width = window.innerWidth; + const height = window.innerHeight; + + // camera + + camera = new THREE.PerspectiveCamera(70, width / height, 1, 100); + camera.position.z = 30; + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(width, height); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // scene + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + + // controls + + controls = new OrbitControls(camera, renderer.domElement); + controls.autoRotate = true; + controls.autoRotateSpeed = 1.0; + + // stats + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // gui + + gui = new GUI(); + gui.add(api, 'count', 1, MAX_GEOMETRY_COUNT).step(1).onChange(initMesh); + gui.add(api, 'dynamic', 0, MAX_GEOMETRY_COUNT).step(1); + gui.add(api, 'method', Method).onChange(initMesh); + gui.add(api, 'opacity', 0, 1).onChange(v => { + if (v < 1) { + material.transparent = true; + material.depthWrite = false; + } else { + material.transparent = false; + material.depthWrite = true; + } + + material.opacity = v; + material.needsUpdate = true; + }); + gui.add(api, 'sortObjects'); + gui.add(api, 'perObjectFrustumCulled'); + gui.add(api, 'useCustomSort'); + + guiStatsEl = document.createElement('li'); + guiStatsEl.classList.add('gui-stats'); + + // listeners + + window.addEventListener('resize', onWindowResize); +} + +// + +function sortFunction(list) { + // initialize options + this._options = this._options || { + get: el => el.z, + aux: new Array(this.maxInstanceCount), + }; + + const options = this._options; + options.reversed = this.material.transparent; + + let minZ = Infinity; + let maxZ = -Infinity; + for (let i = 0, l = list.length; i < l; i++) { + const z = list[i].z; + if (z > maxZ) maxZ = z; + if (z < minZ) minZ = z; + } + + // convert depth to unsigned 32 bit range + const depthDelta = maxZ - minZ; + const factor = (2 ** 32 - 1) / depthDelta; // UINT32_MAX / z range + for (let i = 0, l = list.length; i < l; i++) { + list[i].z -= minZ; + list[i].z *= factor; + } + + // perform a fast-sort using the hybrid radix sort function + radixSort(list, options); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); +} + +function animate() { + animateMeshes(); + + controls.update(); + stats.update(); + + render(); +} + +function animateMeshes() { + const loopNum = Math.min(api.count, api.dynamic); + + if (api.method === Method.BATCHED) { + for (let i = 0; i < loopNum; i++) { + const rotationMatrix = mesh.userData.rotationSpeeds[i]; + const id = ids[i]; + + mesh.getMatrixAt(id, matrix); + matrix.multiply(rotationMatrix); + mesh.setMatrixAt(id, matrix); + } + } else { + for (let i = 0; i < loopNum; i++) { + const child = mesh.children[i]; + const rotationSpeed = child.userData.rotationSpeed; + + child.rotation.set( + child.rotation.x + rotationSpeed.x, + child.rotation.y + rotationSpeed.y, + child.rotation.z + rotationSpeed.z, + ); + } + } +} + +function render() { + if (mesh.isBatchedMesh) { + mesh.sortObjects = api.sortObjects; + mesh.perObjectFrustumCulled = api.perObjectFrustumCulled; + mesh.setCustomSort(api.useCustomSort ? sortFunction : null); + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_mirror.ts b/examples-testing/examples/webgl_mirror.ts new file mode 100644 index 000000000..8b27363a8 --- /dev/null +++ b/examples-testing/examples/webgl_mirror.ts @@ -0,0 +1,168 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { Reflector } from 'three/addons/objects/Reflector.js'; + +let camera, scene, renderer; + +let cameraControls; + +let sphereGroup, smallSphere; + +let groundMirror, verticalMirror; + +init(); + +function init() { + const container = document.getElementById('container'); + + // renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // scene + scene = new THREE.Scene(); + + // camera + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 500); + camera.position.set(0, 75, 160); + + cameraControls = new OrbitControls(camera, renderer.domElement); + cameraControls.target.set(0, 40, 0); + cameraControls.maxDistance = 400; + cameraControls.minDistance = 10; + cameraControls.update(); + + // + + const planeGeo = new THREE.PlaneGeometry(100.1, 100.1); + + // reflectors/mirrors + + let geometry, material; + + geometry = new THREE.CircleGeometry(40, 64); + groundMirror = new Reflector(geometry, { + clipBias: 0.003, + textureWidth: window.innerWidth * window.devicePixelRatio, + textureHeight: window.innerHeight * window.devicePixelRatio, + color: 0xb5b5b5, + }); + groundMirror.position.y = 0.5; + groundMirror.rotateX(-Math.PI / 2); + scene.add(groundMirror); + + geometry = new THREE.PlaneGeometry(100, 100); + verticalMirror = new Reflector(geometry, { + clipBias: 0.003, + textureWidth: window.innerWidth * window.devicePixelRatio, + textureHeight: window.innerHeight * window.devicePixelRatio, + color: 0xc1cbcb, + }); + verticalMirror.position.y = 50; + verticalMirror.position.z = -50; + scene.add(verticalMirror); + + sphereGroup = new THREE.Object3D(); + scene.add(sphereGroup); + + geometry = new THREE.CylinderGeometry(0.1, 15 * Math.cos((Math.PI / 180) * 30), 0.1, 24, 1); + material = new THREE.MeshPhongMaterial({ color: 0xffffff, emissive: 0x8d8d8d }); + const sphereCap = new THREE.Mesh(geometry, material); + sphereCap.position.y = -15 * Math.sin((Math.PI / 180) * 30) - 0.05; + sphereCap.rotateX(-Math.PI); + + geometry = new THREE.SphereGeometry(15, 24, 24, Math.PI / 2, Math.PI * 2, 0, (Math.PI / 180) * 120); + const halfSphere = new THREE.Mesh(geometry, material); + halfSphere.add(sphereCap); + halfSphere.rotateX((-Math.PI / 180) * 135); + halfSphere.rotateZ((-Math.PI / 180) * 20); + halfSphere.position.y = 7.5 + 15 * Math.sin((Math.PI / 180) * 30); + + sphereGroup.add(halfSphere); + + geometry = new THREE.IcosahedronGeometry(5, 0); + material = new THREE.MeshPhongMaterial({ color: 0xffffff, emissive: 0x7b7b7b, flatShading: true }); + smallSphere = new THREE.Mesh(geometry, material); + scene.add(smallSphere); + + // walls + const planeTop = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0xffffff })); + planeTop.position.y = 100; + planeTop.rotateX(Math.PI / 2); + scene.add(planeTop); + + const planeBottom = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0xffffff })); + planeBottom.rotateX(-Math.PI / 2); + scene.add(planeBottom); + + const planeFront = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0x7f7fff })); + planeFront.position.z = 50; + planeFront.position.y = 50; + planeFront.rotateY(Math.PI); + scene.add(planeFront); + + const planeRight = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0x00ff00 })); + planeRight.position.x = 50; + planeRight.position.y = 50; + planeRight.rotateY(-Math.PI / 2); + scene.add(planeRight); + + const planeLeft = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0xff0000 })); + planeLeft.position.x = -50; + planeLeft.position.y = 50; + planeLeft.rotateY(Math.PI / 2); + scene.add(planeLeft); + + // lights + const mainLight = new THREE.PointLight(0xe7e7e7, 2.5, 250, 0); + mainLight.position.y = 60; + scene.add(mainLight); + + const greenLight = new THREE.PointLight(0x00ff00, 0.5, 1000, 0); + greenLight.position.set(550, 50, 0); + scene.add(greenLight); + + const redLight = new THREE.PointLight(0xff0000, 0.5, 1000, 0); + redLight.position.set(-550, 50, 0); + scene.add(redLight); + + const blueLight = new THREE.PointLight(0xbbbbfe, 0.5, 1000, 0); + blueLight.position.set(0, 50, 550); + scene.add(blueLight); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + groundMirror + .getRenderTarget() + .setSize(window.innerWidth * window.devicePixelRatio, window.innerHeight * window.devicePixelRatio); + verticalMirror + .getRenderTarget() + .setSize(window.innerWidth * window.devicePixelRatio, window.innerHeight * window.devicePixelRatio); +} + +function animate() { + const timer = Date.now() * 0.01; + + sphereGroup.rotation.y -= 0.002; + + smallSphere.position.set( + Math.cos(timer * 0.1) * 30, + Math.abs(Math.cos(timer * 0.2)) * 20 + 5, + Math.sin(timer * 0.1) * 30, + ); + smallSphere.rotation.y = Math.PI / 2 - timer * 0.1; + smallSphere.rotation.z = timer * 0.8; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_modifier_edgesplit.ts b/examples-testing/examples/webgl_modifier_edgesplit.ts new file mode 100644 index 000000000..4725eff62 --- /dev/null +++ b/examples-testing/examples/webgl_modifier_edgesplit.ts @@ -0,0 +1,136 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'; +import { EdgeSplitModifier } from 'three/addons/modifiers/EdgeSplitModifier.js'; +import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let renderer, scene, camera; +let modifier, mesh, baseGeometry; +let map; + +const params = { + smoothShading: true, + edgeSplit: true, + cutOffAngle: 20, + showMap: false, + tryKeepNormals: true, +}; + +init(); + +function init() { + const info = document.createElement('div'); + info.style.position = 'absolute'; + info.style.top = '10px'; + info.style.width = '100%'; + info.style.textAlign = 'center'; + info.innerHTML = 'three.js - Edge Split modifier'; + document.body.appendChild(info); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use if there is no animation loop + controls.enableDamping = true; + controls.dampingFactor = 0.25; + controls.rotateSpeed = 0.35; + controls.minZoom = 1; + camera.position.set(0, 0, 4); + + scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 3)); + + new OBJLoader().load('./models/obj/cerberus/Cerberus.obj', function (group) { + const cerberus = group.children[0]; + const modelGeometry = cerberus.geometry; + + modifier = new EdgeSplitModifier(); + baseGeometry = BufferGeometryUtils.mergeVertices(modelGeometry); + + mesh = new THREE.Mesh(getGeometry(), new THREE.MeshStandardMaterial()); + mesh.material.flatShading = !params.smoothShading; + mesh.rotateY(-Math.PI / 2); + mesh.scale.set(3.5, 3.5, 3.5); + mesh.translateZ(1.5); + scene.add(mesh); + + if (map !== undefined && params.showMap) { + mesh.material.map = map; + mesh.material.needsUpdate = true; + } + + render(); + }); + + window.addEventListener('resize', onWindowResize); + + new THREE.TextureLoader().load('./models/obj/cerberus/Cerberus_A.jpg', function (texture) { + map = texture; + map.colorSpace = THREE.SRGBColorSpace; + + if (mesh !== undefined && params.showMap) { + mesh.material.map = map; + mesh.material.needsUpdate = true; + } + }); + + const gui = new GUI({ title: 'Edge split modifier parameters' }); + + gui.add(params, 'showMap').onFinishChange(updateMesh); + gui.add(params, 'smoothShading').onFinishChange(updateMesh); + gui.add(params, 'edgeSplit').onFinishChange(updateMesh); + gui.add(params, 'cutOffAngle').min(0).max(180).onFinishChange(updateMesh); + gui.add(params, 'tryKeepNormals').onFinishChange(updateMesh); +} + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + render(); +} + +function getGeometry() { + let geometry; + + if (params.edgeSplit) { + geometry = modifier.modify(baseGeometry, (params.cutOffAngle * Math.PI) / 180, params.tryKeepNormals); + } else { + geometry = baseGeometry; + } + + return geometry; +} + +function updateMesh() { + if (mesh !== undefined) { + mesh.geometry = getGeometry(); + + let needsUpdate = mesh.material.flatShading === params.smoothShading; + mesh.material.flatShading = params.smoothShading === false; + + if (map !== undefined) { + needsUpdate = needsUpdate || mesh.material.map !== (params.showMap ? map : null); + mesh.material.map = params.showMap ? map : null; + } + + mesh.material.needsUpdate = needsUpdate; + + render(); + } +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_modifier_simplifier.ts b/examples-testing/examples/webgl_modifier_simplifier.ts new file mode 100644 index 000000000..e6ea453b3 --- /dev/null +++ b/examples-testing/examples/webgl_modifier_simplifier.ts @@ -0,0 +1,77 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { SimplifyModifier } from 'three/addons/modifiers/SimplifyModifier.js'; + +let renderer, scene, camera; + +init(); + +function init() { + const info = document.createElement('div'); + info.style.position = 'absolute'; + info.style.top = '10px'; + info.style.width = '100%'; + info.style.textAlign = 'center'; + info.innerHTML = + 'three.js - Vertex Reduction using SimplifyModifier'; + document.body.appendChild(info); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 15; + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use if there is no animation loop + controls.enablePan = false; + controls.enableZoom = false; + + scene.add(new THREE.AmbientLight(0xffffff, 0.6)); + + const light = new THREE.PointLight(0xffffff, 400); + camera.add(light); + scene.add(camera); + + new GLTFLoader().load('models/gltf/LeePerrySmith/LeePerrySmith.glb', function (gltf) { + const mesh = gltf.scene.children[0]; + mesh.position.x = -3; + mesh.rotation.y = Math.PI / 2; + scene.add(mesh); + + const modifier = new SimplifyModifier(); + + const simplified = mesh.clone(); + simplified.material = simplified.material.clone(); + simplified.material.flatShading = true; + const count = Math.floor(simplified.geometry.attributes.position.count * 0.875); // number of vertices to remove + simplified.geometry = modifier.modify(simplified.geometry, count); + + simplified.position.x = 3; + simplified.rotation.y = -Math.PI / 2; + scene.add(simplified); + + render(); + }); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_modifier_tessellation.ts b/examples-testing/examples/webgl_modifier_tessellation.ts new file mode 100644 index 000000000..4600fc6cb --- /dev/null +++ b/examples-testing/examples/webgl_modifier_tessellation.ts @@ -0,0 +1,142 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { TrackballControls } from 'three/addons/controls/TrackballControls.js'; +import { TessellateModifier } from 'three/addons/modifiers/TessellateModifier.js'; +import { FontLoader } from 'three/addons/loaders/FontLoader.js'; +import { TextGeometry } from 'three/addons/geometries/TextGeometry.js'; + +let renderer, scene, camera, stats; + +let controls; + +let mesh, uniforms; + +const WIDTH = window.innerWidth; +const HEIGHT = window.innerHeight; + +const loader = new FontLoader(); +loader.load('fonts/helvetiker_bold.typeface.json', function (font) { + init(font); +}); + +function init(font) { + camera = new THREE.PerspectiveCamera(40, WIDTH / HEIGHT, 1, 10000); + camera.position.set(-100, 100, 200); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x050505); + + // + + let geometry = new TextGeometry('THREE.JS', { + font: font, + + size: 40, + depth: 5, + curveSegments: 3, + + bevelThickness: 2, + bevelSize: 1, + bevelEnabled: true, + }); + + geometry.center(); + + const tessellateModifier = new TessellateModifier(8, 6); + + geometry = tessellateModifier.modify(geometry); + + // + + const numFaces = geometry.attributes.position.count / 3; + + const colors = new Float32Array(numFaces * 3 * 3); + const displacement = new Float32Array(numFaces * 3 * 3); + + const color = new THREE.Color(); + + for (let f = 0; f < numFaces; f++) { + const index = 9 * f; + + const h = 0.2 * Math.random(); + const s = 0.5 + 0.5 * Math.random(); + const l = 0.5 + 0.5 * Math.random(); + + color.setHSL(h, s, l); + + const d = 10 * (0.5 - Math.random()); + + for (let i = 0; i < 3; i++) { + colors[index + 3 * i] = color.r; + colors[index + 3 * i + 1] = color.g; + colors[index + 3 * i + 2] = color.b; + + displacement[index + 3 * i] = d; + displacement[index + 3 * i + 1] = d; + displacement[index + 3 * i + 2] = d; + } + } + + geometry.setAttribute('customColor', new THREE.BufferAttribute(colors, 3)); + geometry.setAttribute('displacement', new THREE.BufferAttribute(displacement, 3)); + + // + + uniforms = { + amplitude: { value: 0.0 }, + }; + + const shaderMaterial = new THREE.ShaderMaterial({ + uniforms: uniforms, + vertexShader: document.getElementById('vertexshader').textContent, + fragmentShader: document.getElementById('fragmentshader').textContent, + }); + + // + + mesh = new THREE.Mesh(geometry, shaderMaterial); + + scene.add(mesh); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(WIDTH, HEIGHT); + renderer.setAnimationLoop(animate); + + const container = document.getElementById('container'); + container.appendChild(renderer.domElement); + + controls = new TrackballControls(camera, renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + render(); + + stats.update(); +} + +function render() { + const time = Date.now() * 0.001; + + uniforms.amplitude.value = 1.0 + Math.sin(time * 0.5); + + controls.update(); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_morphtargets.ts b/examples-testing/examples/webgl_morphtargets.ts new file mode 100644 index 000000000..40d605f8d --- /dev/null +++ b/examples-testing/examples/webgl_morphtargets.ts @@ -0,0 +1,120 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let container, camera, scene, renderer, mesh; + +init(); + +function init() { + container = document.getElementById('container'); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x8fbcd4); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 20); + camera.position.z = 10; + scene.add(camera); + + scene.add(new THREE.AmbientLight(0x8fbcd4, 1.5)); + + const pointLight = new THREE.PointLight(0xffffff, 200); + camera.add(pointLight); + + const geometry = createGeometry(); + + const material = new THREE.MeshPhongMaterial({ + color: 0xff0000, + flatShading: true, + }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + initGUI(); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(function () { + renderer.render(scene, camera); + }); + container.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableZoom = false; + + window.addEventListener('resize', onWindowResize); +} + +function createGeometry() { + const geometry = new THREE.BoxGeometry(2, 2, 2, 32, 32, 32); + + // create an empty array to hold targets for the attribute we want to morph + // morphing positions and normals is supported + geometry.morphAttributes.position = []; + + // the original positions of the cube's vertices + const positionAttribute = geometry.attributes.position; + + // for the first morph target we'll move the cube's vertices onto the surface of a sphere + const spherePositions = []; + + // for the second morph target, we'll twist the cubes vertices + const twistPositions = []; + const direction = new THREE.Vector3(1, 0, 0); + const vertex = new THREE.Vector3(); + + for (let i = 0; i < positionAttribute.count; i++) { + const x = positionAttribute.getX(i); + const y = positionAttribute.getY(i); + const z = positionAttribute.getZ(i); + + spherePositions.push( + x * Math.sqrt(1 - (y * y) / 2 - (z * z) / 2 + (y * y * z * z) / 3), + y * Math.sqrt(1 - (z * z) / 2 - (x * x) / 2 + (z * z * x * x) / 3), + z * Math.sqrt(1 - (x * x) / 2 - (y * y) / 2 + (x * x * y * y) / 3), + ); + + // stretch along the x-axis so we can see the twist better + vertex.set(x * 2, y, z); + + vertex.applyAxisAngle(direction, (Math.PI * x) / 2).toArray(twistPositions, twistPositions.length); + } + + // add the spherical positions as the first morph target + geometry.morphAttributes.position[0] = new THREE.Float32BufferAttribute(spherePositions, 3); + + // add the twisted positions as the second morph target + geometry.morphAttributes.position[1] = new THREE.Float32BufferAttribute(twistPositions, 3); + + return geometry; +} + +function initGUI() { + // Set up dat.GUI to control targets + const params = { + Spherify: 0, + Twist: 0, + }; + const gui = new GUI({ title: 'Morph Targets' }); + + gui.add(params, 'Spherify', 0, 1) + .step(0.01) + .onChange(function (value) { + mesh.morphTargetInfluences[0] = value; + }); + gui.add(params, 'Twist', 0, 1) + .step(0.01) + .onChange(function (value) { + mesh.morphTargetInfluences[1] = value; + }); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} diff --git a/examples-testing/examples/webgl_morphtargets_face.ts b/examples-testing/examples/webgl_morphtargets_face.ts new file mode 100644 index 000000000..76179d902 --- /dev/null +++ b/examples-testing/examples/webgl_morphtargets_face.ts @@ -0,0 +1,105 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'; +import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'; + +import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, stats, mixer, clock, controls; + +init(); + +function init() { + clock = new THREE.Clock(); + + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 20); + camera.position.set(-1.8, 0.8, 3); + + scene = new THREE.Scene(); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + + container.appendChild(renderer.domElement); + + const ktx2Loader = new KTX2Loader().setTranscoderPath('jsm/libs/basis/').detectSupport(renderer); + + new GLTFLoader() + .setKTX2Loader(ktx2Loader) + .setMeshoptDecoder(MeshoptDecoder) + .load('models/gltf/facecap.glb', gltf => { + const mesh = gltf.scene.children[0]; + + scene.add(mesh); + + mixer = new THREE.AnimationMixer(mesh); + + mixer.clipAction(gltf.animations[0]).play(); + + // GUI + + const head = mesh.getObjectByName('mesh_2'); + const influences = head.morphTargetInfluences; + + const gui = new GUI(); + gui.close(); + + for (const [key, value] of Object.entries(head.morphTargetDictionary)) { + gui.add(influences, value, 0, 1, 0.01).name(key.replace('blendShape1.', '')).listen(); + } + }); + + const environment = new RoomEnvironment(); + const pmremGenerator = new THREE.PMREMGenerator(renderer); + + scene.background = new THREE.Color(0x666666); + scene.environment = pmremGenerator.fromScene(environment).texture; + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.minDistance = 2.5; + controls.maxDistance = 5; + controls.minAzimuthAngle = -Math.PI / 2; + controls.maxAzimuthAngle = Math.PI / 2; + controls.maxPolarAngle = Math.PI / 1.8; + controls.target.set(0, 0.15, -0.2); + + stats = new Stats(); + container.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const delta = clock.getDelta(); + + if (mixer) { + mixer.update(delta); + } + + renderer.render(scene, camera); + + controls.update(); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_morphtargets_horse.ts b/examples-testing/examples/webgl_morphtargets_horse.ts new file mode 100644 index 000000000..2c29e9c0e --- /dev/null +++ b/examples-testing/examples/webgl_morphtargets_horse.ts @@ -0,0 +1,100 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +let container, stats; +let camera, scene, renderer; +let mesh, mixer; + +const radius = 600; +let theta = 0; +let prevTime = Date.now(); + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + // + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.y = 300; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xf0f0f0); + + // + + const light1 = new THREE.DirectionalLight(0xefefff, 5); + light1.position.set(1, 1, 1).normalize(); + scene.add(light1); + + const light2 = new THREE.DirectionalLight(0xffefef, 5); + light2.position.set(-1, -1, -1).normalize(); + scene.add(light2); + + const loader = new GLTFLoader(); + loader.load('models/gltf/Horse.glb', function (gltf) { + mesh = gltf.scene.children[0]; + mesh.scale.set(1.5, 1.5, 1.5); + scene.add(mesh); + + mixer = new THREE.AnimationMixer(mesh); + + mixer.clipAction(gltf.animations[0]).setDuration(1).play(); + }); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + + container.appendChild(renderer.domElement); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + theta += 0.1; + + camera.position.x = radius * Math.sin(THREE.MathUtils.degToRad(theta)); + camera.position.z = radius * Math.cos(THREE.MathUtils.degToRad(theta)); + + camera.lookAt(0, 150, 0); + + if (mixer) { + const time = Date.now(); + + mixer.update((time - prevTime) * 0.001); + + prevTime = time; + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_morphtargets_sphere.ts b/examples-testing/examples/webgl_morphtargets_sphere.ts new file mode 100644 index 000000000..2b8899111 --- /dev/null +++ b/examples-testing/examples/webgl_morphtargets_sphere.ts @@ -0,0 +1,105 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { Timer } from 'three/addons/misc/Timer.js'; + +let camera, scene, renderer, timer; + +let mesh; + +let sign = 1; +const speed = 0.5; + +init(); + +function init() { + const container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.2, 100); + camera.position.set(0, 5, 5); + + scene = new THREE.Scene(); + + timer = new Timer(); + + const light1 = new THREE.PointLight(0xff2200, 50000); + light1.position.set(100, 100, 100); + scene.add(light1); + + const light2 = new THREE.PointLight(0x22ff00, 10000); + light2.position.set(-100, -100, -100); + scene.add(light2); + + scene.add(new THREE.AmbientLight(0x111111)); + + const loader = new GLTFLoader(); + loader.load('models/gltf/AnimatedMorphSphere/glTF/AnimatedMorphSphere.gltf', function (gltf) { + mesh = gltf.scene.getObjectByName('AnimatedMorphSphere'); + mesh.rotation.z = Math.PI / 2; + scene.add(mesh); + + // + + const pointsMaterial = new THREE.PointsMaterial({ + size: 10, + sizeAttenuation: false, + map: new THREE.TextureLoader().load('textures/sprites/disc.png'), + alphaTest: 0.5, + }); + + const points = new THREE.Points(mesh.geometry, pointsMaterial); + points.morphTargetInfluences = mesh.morphTargetInfluences; + points.morphTargetDictionary = mesh.morphTargetDictionary; + mesh.add(points); + }); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + + container.appendChild(renderer.domElement); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 1; + controls.maxDistance = 20; + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + timer.update(); + render(); +} + +function render() { + const delta = timer.getDelta(); + + if (mesh !== undefined) { + const step = delta * speed; + + mesh.rotation.y += step; + + mesh.morphTargetInfluences[1] = mesh.morphTargetInfluences[1] + step * sign; + + if (mesh.morphTargetInfluences[1] <= 0 || mesh.morphTargetInfluences[1] >= 1) { + sign *= -1; + } + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_multiple_elements.ts b/examples-testing/examples/webgl_multiple_elements.ts new file mode 100644 index 000000000..64f8a9c5f --- /dev/null +++ b/examples-testing/examples/webgl_multiple_elements.ts @@ -0,0 +1,139 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let canvas, renderer; + +const scenes = []; + +init(); + +function init() { + canvas = document.getElementById('c'); + + const geometries = [ + new THREE.BoxGeometry(1, 1, 1), + new THREE.SphereGeometry(0.5, 12, 8), + new THREE.DodecahedronGeometry(0.5), + new THREE.CylinderGeometry(0.5, 0.5, 1, 12), + ]; + + const content = document.getElementById('content'); + + for (let i = 0; i < 40; i++) { + const scene = new THREE.Scene(); + + // make a list item + const element = document.createElement('div'); + element.className = 'list-item'; + + const sceneElement = document.createElement('div'); + element.appendChild(sceneElement); + + const descriptionElement = document.createElement('div'); + descriptionElement.innerText = 'Scene ' + (i + 1); + element.appendChild(descriptionElement); + + // the element that represents the area we want to render the scene + scene.userData.element = sceneElement; + content.appendChild(element); + + const camera = new THREE.PerspectiveCamera(50, 1, 1, 10); + camera.position.z = 2; + scene.userData.camera = camera; + + const controls = new OrbitControls(scene.userData.camera, scene.userData.element); + controls.minDistance = 2; + controls.maxDistance = 5; + controls.enablePan = false; + controls.enableZoom = false; + scene.userData.controls = controls; + + // add one random mesh to each scene + const geometry = geometries[(geometries.length * Math.random()) | 0]; + + const material = new THREE.MeshStandardMaterial({ + color: new THREE.Color().setHSL(Math.random(), 1, 0.75, THREE.SRGBColorSpace), + roughness: 0.5, + metalness: 0, + flatShading: true, + }); + + scene.add(new THREE.Mesh(geometry, material)); + + scene.add(new THREE.HemisphereLight(0xaaaaaa, 0x444444, 3)); + + const light = new THREE.DirectionalLight(0xffffff, 1.5); + light.position.set(1, 1, 1); + scene.add(light); + + scenes.push(scene); + } + + renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true }); + renderer.setClearColor(0xffffff, 1); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setAnimationLoop(animate); +} + +function updateSize() { + const width = canvas.clientWidth; + const height = canvas.clientHeight; + + if (canvas.width !== width || canvas.height !== height) { + renderer.setSize(width, height, false); + } +} + +function animate() { + updateSize(); + + canvas.style.transform = `translateY(${window.scrollY}px)`; + + renderer.setClearColor(0xffffff); + renderer.setScissorTest(false); + renderer.clear(); + + renderer.setClearColor(0xe0e0e0); + renderer.setScissorTest(true); + + scenes.forEach(function (scene) { + // so something moves + scene.children[0].rotation.y = Date.now() * 0.001; + + // get the element that is a place holder for where we want to + // draw the scene + const element = scene.userData.element; + + // get its position relative to the page's viewport + const rect = element.getBoundingClientRect(); + + // check if it's offscreen. If so skip it + if ( + rect.bottom < 0 || + rect.top > renderer.domElement.clientHeight || + rect.right < 0 || + rect.left > renderer.domElement.clientWidth + ) { + return; // it's off screen + } + + // set the viewport + const width = rect.right - rect.left; + const height = rect.bottom - rect.top; + const left = rect.left; + const bottom = renderer.domElement.clientHeight - rect.bottom; + + renderer.setViewport(left, bottom, width, height); + renderer.setScissor(left, bottom, width, height); + + const camera = scene.userData.camera; + + //camera.aspect = width / height; // not changing in this example + //camera.updateProjectionMatrix(); + + //scene.userData.controls.update(); + + renderer.render(scene, camera); + }); +} diff --git a/examples-testing/examples/webgl_multiple_rendertargets.ts b/examples-testing/examples/webgl_multiple_rendertargets.ts new file mode 100644 index 000000000..86708082b --- /dev/null +++ b/examples-testing/examples/webgl_multiple_rendertargets.ts @@ -0,0 +1,133 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, controls; +let renderTarget; +let postScene, postCamera; + +const parameters = { + samples: 4, + wireframe: false, +}; + +const gui = new GUI(); +gui.add(parameters, 'samples', 0, 4).step(1); +gui.add(parameters, 'wireframe'); +gui.onChange(render); + +init(); + +function init() { + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + // Create a multi render target with Float buffers + + renderTarget = new THREE.WebGLRenderTarget( + window.innerWidth * window.devicePixelRatio, + window.innerHeight * window.devicePixelRatio, + { + count: 2, + minFilter: THREE.NearestFilter, + magFilter: THREE.NearestFilter, + }, + ); + + // Name our G-Buffer attachments for debugging + + renderTarget.textures[0].name = 'diffuse'; + renderTarget.textures[1].name = 'normal'; + + // Scene setup + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x222222); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 50); + camera.position.z = 4; + + const loader = new THREE.TextureLoader(); + + const diffuse = loader.load('textures/hardwood2_diffuse.jpg', render); + diffuse.wrapS = THREE.RepeatWrapping; + diffuse.wrapT = THREE.RepeatWrapping; + diffuse.colorSpace = THREE.SRGBColorSpace; + + scene.add( + new THREE.Mesh( + new THREE.TorusKnotGeometry(1, 0.3, 128, 32), + new THREE.RawShaderMaterial({ + name: 'G-Buffer Shader', + vertexShader: document.querySelector('#gbuffer-vert').textContent.trim(), + fragmentShader: document.querySelector('#gbuffer-frag').textContent.trim(), + uniforms: { + tDiffuse: { value: diffuse }, + repeat: { value: new THREE.Vector2(5, 0.5) }, + }, + glslVersion: THREE.GLSL3, + }), + ), + ); + + // PostProcessing setup + + postScene = new THREE.Scene(); + postCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); + + postScene.add( + new THREE.Mesh( + new THREE.PlaneGeometry(2, 2), + new THREE.RawShaderMaterial({ + name: 'Post-FX Shader', + vertexShader: document.querySelector('#render-vert').textContent.trim(), + fragmentShader: document.querySelector('#render-frag').textContent.trim(), + uniforms: { + tDiffuse: { value: renderTarget.textures[0] }, + tNormal: { value: renderTarget.textures[1] }, + }, + glslVersion: THREE.GLSL3, + }), + ), + ); + + // Controls + + controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + const dpr = renderer.getPixelRatio(); + renderTarget.setSize(window.innerWidth * dpr, window.innerHeight * dpr); + + render(); +} + +function render() { + renderTarget.samples = parameters.samples; + + scene.traverse(function (child) { + if (child.material !== undefined) { + child.material.wireframe = parameters.wireframe; + } + }); + + // render scene into target + renderer.setRenderTarget(renderTarget); + renderer.render(scene, camera); + + // render post FX + renderer.setRenderTarget(null); + renderer.render(postScene, postCamera); +} diff --git a/examples-testing/examples/webgl_multiple_scenes_comparison.ts b/examples-testing/examples/webgl_multiple_scenes_comparison.ts new file mode 100644 index 000000000..41a5130d4 --- /dev/null +++ b/examples-testing/examples/webgl_multiple_scenes_comparison.ts @@ -0,0 +1,98 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let container, camera, renderer, controls; +let sceneL, sceneR; + +let sliderPos = window.innerWidth / 2; + +init(); + +function init() { + container = document.querySelector('.container'); + + sceneL = new THREE.Scene(); + sceneL.background = new THREE.Color(0xbcd48f); + + sceneR = new THREE.Scene(); + sceneR.background = new THREE.Color(0x8fbcd4); + + camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.z = 6; + + controls = new OrbitControls(camera, container); + + const light = new THREE.HemisphereLight(0xffffff, 0x444444, 3); + light.position.set(-2, 2, 2); + sceneL.add(light.clone()); + sceneR.add(light.clone()); + + initMeshes(); + initSlider(); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setScissorTest(true); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); +} + +function initMeshes() { + const geometry = new THREE.IcosahedronGeometry(1, 3); + + const meshL = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial()); + sceneL.add(meshL); + + const meshR = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ wireframe: true })); + sceneR.add(meshR); +} + +function initSlider() { + const slider = document.querySelector('.slider'); + + function onPointerDown() { + if (event.isPrimary === false) return; + + controls.enabled = false; + + window.addEventListener('pointermove', onPointerMove); + window.addEventListener('pointerup', onPointerUp); + } + + function onPointerUp() { + controls.enabled = true; + + window.removeEventListener('pointermove', onPointerMove); + window.removeEventListener('pointerup', onPointerUp); + } + + function onPointerMove(e) { + if (event.isPrimary === false) return; + + sliderPos = Math.max(0, Math.min(window.innerWidth, e.pageX)); + + slider.style.left = sliderPos - slider.offsetWidth / 2 + 'px'; + } + + slider.style.touchAction = 'none'; // disable touch scroll + slider.addEventListener('pointerdown', onPointerDown); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.setScissor(0, 0, sliderPos, window.innerHeight); + renderer.render(sceneL, camera); + + renderer.setScissor(sliderPos, 0, window.innerWidth, window.innerHeight); + renderer.render(sceneR, camera); +} diff --git a/examples-testing/examples/webgl_multiple_views.ts b/examples-testing/examples/webgl_multiple_views.ts new file mode 100644 index 000000000..29126b013 --- /dev/null +++ b/examples-testing/examples/webgl_multiple_views.ts @@ -0,0 +1,237 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let stats; + +let scene, renderer; + +let mouseX = 0, + mouseY = 0; + +let windowWidth, windowHeight; + +const views = [ + { + left: 0, + bottom: 0, + width: 0.5, + height: 1.0, + background: new THREE.Color().setRGB(0.5, 0.5, 0.7, THREE.SRGBColorSpace), + eye: [0, 300, 1800], + up: [0, 1, 0], + fov: 30, + updateCamera: function (camera, scene, mouseX) { + camera.position.x += mouseX * 0.05; + camera.position.x = Math.max(Math.min(camera.position.x, 2000), -2000); + camera.lookAt(scene.position); + }, + }, + { + left: 0.5, + bottom: 0, + width: 0.5, + height: 0.5, + background: new THREE.Color().setRGB(0.7, 0.5, 0.5, THREE.SRGBColorSpace), + eye: [0, 1800, 0], + up: [0, 0, 1], + fov: 45, + updateCamera: function (camera, scene, mouseX) { + camera.position.x -= mouseX * 0.05; + camera.position.x = Math.max(Math.min(camera.position.x, 2000), -2000); + camera.lookAt(camera.position.clone().setY(0)); + }, + }, + { + left: 0.5, + bottom: 0.5, + width: 0.5, + height: 0.5, + background: new THREE.Color().setRGB(0.5, 0.7, 0.7, THREE.SRGBColorSpace), + eye: [1400, 800, 1400], + up: [0, 1, 0], + fov: 60, + updateCamera: function (camera, scene, mouseX) { + camera.position.y -= mouseX * 0.05; + camera.position.y = Math.max(Math.min(camera.position.y, 1600), -1600); + camera.lookAt(scene.position); + }, + }, +]; + +init(); + +function init() { + const container = document.getElementById('container'); + + for (let ii = 0; ii < views.length; ++ii) { + const view = views[ii]; + const camera = new THREE.PerspectiveCamera(view.fov, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.fromArray(view.eye); + camera.up.fromArray(view.up); + view.camera = camera; + } + + scene = new THREE.Scene(); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0, 0, 1); + scene.add(light); + + // shadow + + const canvas = document.createElement('canvas'); + canvas.width = 128; + canvas.height = 128; + + const context = canvas.getContext('2d'); + const gradient = context.createRadialGradient( + canvas.width / 2, + canvas.height / 2, + 0, + canvas.width / 2, + canvas.height / 2, + canvas.width / 2, + ); + gradient.addColorStop(0.1, 'rgba(0,0,0,0.15)'); + gradient.addColorStop(1, 'rgba(0,0,0,0)'); + + context.fillStyle = gradient; + context.fillRect(0, 0, canvas.width, canvas.height); + + const shadowTexture = new THREE.CanvasTexture(canvas); + + const shadowMaterial = new THREE.MeshBasicMaterial({ map: shadowTexture, transparent: true }); + const shadowGeo = new THREE.PlaneGeometry(300, 300, 1, 1); + + let shadowMesh; + + shadowMesh = new THREE.Mesh(shadowGeo, shadowMaterial); + shadowMesh.position.y = -250; + shadowMesh.rotation.x = -Math.PI / 2; + scene.add(shadowMesh); + + shadowMesh = new THREE.Mesh(shadowGeo, shadowMaterial); + shadowMesh.position.x = -400; + shadowMesh.position.y = -250; + shadowMesh.rotation.x = -Math.PI / 2; + scene.add(shadowMesh); + + shadowMesh = new THREE.Mesh(shadowGeo, shadowMaterial); + shadowMesh.position.x = 400; + shadowMesh.position.y = -250; + shadowMesh.rotation.x = -Math.PI / 2; + scene.add(shadowMesh); + + const radius = 200; + + const geometry1 = new THREE.IcosahedronGeometry(radius, 1); + + const count = geometry1.attributes.position.count; + geometry1.setAttribute('color', new THREE.BufferAttribute(new Float32Array(count * 3), 3)); + + const geometry2 = geometry1.clone(); + const geometry3 = geometry1.clone(); + + const color = new THREE.Color(); + const positions1 = geometry1.attributes.position; + const positions2 = geometry2.attributes.position; + const positions3 = geometry3.attributes.position; + const colors1 = geometry1.attributes.color; + const colors2 = geometry2.attributes.color; + const colors3 = geometry3.attributes.color; + + for (let i = 0; i < count; i++) { + color.setHSL((positions1.getY(i) / radius + 1) / 2, 1.0, 0.5, THREE.SRGBColorSpace); + colors1.setXYZ(i, color.r, color.g, color.b); + + color.setHSL(0, (positions2.getY(i) / radius + 1) / 2, 0.5, THREE.SRGBColorSpace); + colors2.setXYZ(i, color.r, color.g, color.b); + + color.setRGB(1, 0.8 - (positions3.getY(i) / radius + 1) / 2, 0, THREE.SRGBColorSpace); + colors3.setXYZ(i, color.r, color.g, color.b); + } + + const material = new THREE.MeshPhongMaterial({ + color: 0xffffff, + flatShading: true, + vertexColors: true, + shininess: 0, + }); + + const wireframeMaterial = new THREE.MeshBasicMaterial({ color: 0x000000, wireframe: true, transparent: true }); + + let mesh = new THREE.Mesh(geometry1, material); + let wireframe = new THREE.Mesh(geometry1, wireframeMaterial); + mesh.add(wireframe); + mesh.position.x = -400; + mesh.rotation.x = -1.87; + scene.add(mesh); + + mesh = new THREE.Mesh(geometry2, material); + wireframe = new THREE.Mesh(geometry2, wireframeMaterial); + mesh.add(wireframe); + mesh.position.x = 400; + scene.add(mesh); + + mesh = new THREE.Mesh(geometry3, material); + wireframe = new THREE.Mesh(geometry3, wireframeMaterial); + mesh.add(wireframe); + scene.add(mesh); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + document.addEventListener('mousemove', onDocumentMouseMove); +} + +function onDocumentMouseMove(event) { + mouseX = event.clientX - windowWidth / 2; + mouseY = event.clientY - windowHeight / 2; +} + +function updateSize() { + if (windowWidth != window.innerWidth || windowHeight != window.innerHeight) { + windowWidth = window.innerWidth; + windowHeight = window.innerHeight; + + renderer.setSize(windowWidth, windowHeight); + } +} + +function animate() { + render(); + stats.update(); +} + +function render() { + updateSize(); + + for (let ii = 0; ii < views.length; ++ii) { + const view = views[ii]; + const camera = view.camera; + + view.updateCamera(camera, scene, mouseX, mouseY); + + const left = Math.floor(windowWidth * view.left); + const bottom = Math.floor(windowHeight * view.bottom); + const width = Math.floor(windowWidth * view.width); + const height = Math.floor(windowHeight * view.height); + + renderer.setViewport(left, bottom, width, height); + renderer.setScissor(left, bottom, width, height); + renderer.setScissorTest(true); + renderer.setClearColor(view.background); + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.render(scene, camera); + } +} diff --git a/examples-testing/examples/webgl_multisampled_renderbuffers.ts b/examples-testing/examples/webgl_multisampled_renderbuffers.ts new file mode 100644 index 000000000..df84fb144 --- /dev/null +++ b/examples-testing/examples/webgl_multisampled_renderbuffers.ts @@ -0,0 +1,133 @@ +import * as THREE from 'three'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, renderer, group, container; + +let composer1, composer2; + +const params = { + animate: true, +}; + +init(); + +function init() { + container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(45, container.offsetWidth / container.offsetHeight, 10, 2000); + camera.position.z = 500; + + const scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + scene.fog = new THREE.Fog(0xcccccc, 100, 1500); + + // + + const hemiLight = new THREE.HemisphereLight(0xffffff, 0x222222, 5); + hemiLight.position.set(1, 1, 1); + scene.add(hemiLight); + + // + + group = new THREE.Group(); + + const geometry = new THREE.SphereGeometry(10, 64, 40); + const material = new THREE.MeshLambertMaterial({ + color: 0xee0808, + polygonOffset: true, + polygonOffsetFactor: 1, // positive value pushes polygon further away + polygonOffsetUnits: 1, + }); + const material2 = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true }); + + for (let i = 0; i < 50; i++) { + const mesh = new THREE.Mesh(geometry, material); + mesh.position.x = Math.random() * 600 - 300; + mesh.position.y = Math.random() * 600 - 300; + mesh.position.z = Math.random() * 600 - 300; + mesh.rotation.x = Math.random(); + mesh.rotation.z = Math.random(); + mesh.scale.setScalar(Math.random() * 5 + 5); + group.add(mesh); + + const mesh2 = new THREE.Mesh(geometry, material2); + mesh2.position.copy(mesh.position); + mesh2.rotation.copy(mesh.rotation); + mesh2.scale.copy(mesh.scale); + group.add(mesh2); + } + + scene.add(group); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(container.offsetWidth, container.offsetHeight); + renderer.setAnimationLoop(animate); + renderer.autoClear = false; + container.appendChild(renderer.domElement); + + // + + const size = renderer.getDrawingBufferSize(new THREE.Vector2()); + const renderTarget = new THREE.WebGLRenderTarget(size.width, size.height, { + samples: 4, + type: THREE.HalfFloatType, + }); + + const renderPass = new RenderPass(scene, camera); + const outputPass = new OutputPass(); + + // + + composer1 = new EffectComposer(renderer); + composer1.addPass(renderPass); + composer1.addPass(outputPass); + + // + + composer2 = new EffectComposer(renderer, renderTarget); + composer2.addPass(renderPass); + composer2.addPass(outputPass); + + // + + const gui = new GUI(); + gui.add(params, 'animate'); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = container.offsetWidth / container.offsetHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(container.offsetWidth, container.offsetHeight); + composer1.setSize(container.offsetWidth, container.offsetHeight); + composer2.setSize(container.offsetWidth, container.offsetHeight); +} + +function animate() { + const halfWidth = container.offsetWidth / 2; + + if (params.animate) { + group.rotation.y += 0.002; + } + + renderer.setScissorTest(true); + + renderer.setScissor(0, 0, halfWidth - 1, container.offsetHeight); + composer1.render(); + + renderer.setScissor(halfWidth, 0, halfWidth, container.offsetHeight); + composer2.render(); + + renderer.setScissorTest(false); +} diff --git a/examples-testing/examples/webgl_panorama_cube.ts b/examples-testing/examples/webgl_panorama_cube.ts new file mode 100644 index 000000000..efd09cfc5 --- /dev/null +++ b/examples-testing/examples/webgl_panorama_cube.ts @@ -0,0 +1,83 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, controls; +let renderer; +let scene; + +init(); + +function init() { + const container = document.getElementById('container'); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.z = 0.01; + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableZoom = false; + controls.enablePan = false; + controls.enableDamping = true; + controls.rotateSpeed = -0.25; + + const textures = getTexturesFromAtlasFile('textures/cube/sun_temple_stripe.jpg', 6); + + const materials = []; + + for (let i = 0; i < 6; i++) { + materials.push(new THREE.MeshBasicMaterial({ map: textures[i] })); + } + + const skyBox = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), materials); + skyBox.geometry.scale(1, 1, -1); + scene.add(skyBox); + + window.addEventListener('resize', onWindowResize); +} + +function getTexturesFromAtlasFile(atlasImgUrl, tilesNum) { + const textures = []; + + for (let i = 0; i < tilesNum; i++) { + textures[i] = new THREE.Texture(); + } + + new THREE.ImageLoader().load(atlasImgUrl, image => { + let canvas, context; + const tileWidth = image.height; + + for (let i = 0; i < textures.length; i++) { + canvas = document.createElement('canvas'); + context = canvas.getContext('2d'); + canvas.height = tileWidth; + canvas.width = tileWidth; + context.drawImage(image, tileWidth * i, 0, tileWidth, tileWidth, 0, 0, tileWidth, tileWidth); + textures[i].colorSpace = THREE.SRGBColorSpace; + textures[i].image = canvas; + textures[i].needsUpdate = true; + } + }); + + return textures; +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(); // required when damping is enabled + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_panorama_equirectangular.ts b/examples-testing/examples/webgl_panorama_equirectangular.ts new file mode 100644 index 000000000..552745222 --- /dev/null +++ b/examples-testing/examples/webgl_panorama_equirectangular.ts @@ -0,0 +1,142 @@ +import * as THREE from 'three'; + +let camera, scene, renderer; + +let isUserInteracting = false, + onPointerDownMouseX = 0, + onPointerDownMouseY = 0, + lon = 0, + onPointerDownLon = 0, + lat = 0, + onPointerDownLat = 0, + phi = 0, + theta = 0; + +init(); + +function init() { + const container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1100); + + scene = new THREE.Scene(); + + const geometry = new THREE.SphereGeometry(500, 60, 40); + // invert the geometry on the x-axis so that all of the faces point inward + geometry.scale(-1, 1, 1); + + const texture = new THREE.TextureLoader().load('textures/2294472375_24a3b8ef46_o.jpg'); + texture.colorSpace = THREE.SRGBColorSpace; + const material = new THREE.MeshBasicMaterial({ map: texture }); + + const mesh = new THREE.Mesh(geometry, material); + + scene.add(mesh); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + container.style.touchAction = 'none'; + container.addEventListener('pointerdown', onPointerDown); + + document.addEventListener('wheel', onDocumentMouseWheel); + + // + + document.addEventListener('dragover', function (event) { + event.preventDefault(); + event.dataTransfer.dropEffect = 'copy'; + }); + + document.addEventListener('dragenter', function () { + document.body.style.opacity = 0.5; + }); + + document.addEventListener('dragleave', function () { + document.body.style.opacity = 1; + }); + + document.addEventListener('drop', function (event) { + event.preventDefault(); + + const reader = new FileReader(); + reader.addEventListener('load', function (event) { + material.map.image.src = event.target.result; + material.map.needsUpdate = true; + }); + reader.readAsDataURL(event.dataTransfer.files[0]); + + document.body.style.opacity = 1; + }); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onPointerDown(event) { + if (event.isPrimary === false) return; + + isUserInteracting = true; + + onPointerDownMouseX = event.clientX; + onPointerDownMouseY = event.clientY; + + onPointerDownLon = lon; + onPointerDownLat = lat; + + document.addEventListener('pointermove', onPointerMove); + document.addEventListener('pointerup', onPointerUp); +} + +function onPointerMove(event) { + if (event.isPrimary === false) return; + + lon = (onPointerDownMouseX - event.clientX) * 0.1 + onPointerDownLon; + lat = (event.clientY - onPointerDownMouseY) * 0.1 + onPointerDownLat; +} + +function onPointerUp() { + if (event.isPrimary === false) return; + + isUserInteracting = false; + + document.removeEventListener('pointermove', onPointerMove); + document.removeEventListener('pointerup', onPointerUp); +} + +function onDocumentMouseWheel(event) { + const fov = camera.fov + event.deltaY * 0.05; + + camera.fov = THREE.MathUtils.clamp(fov, 10, 75); + + camera.updateProjectionMatrix(); +} + +function animate() { + if (isUserInteracting === false) { + lon += 0.1; + } + + lat = Math.max(-85, Math.min(85, lat)); + phi = THREE.MathUtils.degToRad(90 - lat); + theta = THREE.MathUtils.degToRad(lon); + + const x = 500 * Math.sin(phi) * Math.cos(theta); + const y = 500 * Math.cos(phi); + const z = 500 * Math.sin(phi) * Math.sin(theta); + + camera.lookAt(x, y, z); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_performance.ts b/examples-testing/examples/webgl_performance.ts new file mode 100644 index 000000000..3700386a3 --- /dev/null +++ b/examples-testing/examples/webgl_performance.ts @@ -0,0 +1,77 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +let camera, scene, renderer, stats; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(60, 60, 60); + + scene = new THREE.Scene(); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1; + + renderer.setAnimationLoop(render); + container.appendChild(renderer.domElement); + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + new RGBELoader().setPath('textures/equirectangular/').load('royal_esplanade_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.environment = texture; + + // model + + const loader = new GLTFLoader().setPath('models/gltf/'); + loader.load('dungeon_warkarma.glb', async function (gltf) { + const model = gltf.scene; + + // wait until the model can be added to the scene without blocking due to shader compilation + + await renderer.compileAsync(model, camera, scene); + + scene.add(model); + }); + }); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 2; + controls.maxDistance = 60; + controls.target.set(0, 0, -0.2); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function render() { + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_pmrem_test.ts b/examples-testing/examples/webgl_pmrem_test.ts new file mode 100644 index 000000000..b33e4e2f1 --- /dev/null +++ b/examples-testing/examples/webgl_pmrem_test.ts @@ -0,0 +1,141 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let scene, camera, controls, renderer; + +function init() { + const width = window.innerWidth; + const height = window.innerHeight; + const aspect = width / height; + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(width, height); + + // tonemapping + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1; + + document.body.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); + + // scene + + scene = new THREE.Scene(); + + // camera + + camera = new THREE.PerspectiveCamera(40, aspect, 1, 30); + updateCamera(); + camera.position.set(0, 0, 16); + + // controls + + controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use if there is no animation loop + controls.minDistance = 4; + controls.maxDistance = 20; + + // light + + const directionalLight = new THREE.DirectionalLight(0xffffff, 0); // set intensity to 0 to start + const x = 597; + const y = 213; + const theta = ((x + 0.5) * Math.PI) / 512; + const phi = ((y + 0.5) * Math.PI) / 512; + + directionalLight.position.setFromSphericalCoords(100, -phi, Math.PI / 2 - theta); + + scene.add(directionalLight); + // scene.add( new THREE.DirectionalLightHelper( directionalLight ) ); + + // The spot1Lux HDR environment map is expressed in nits (lux / sr). The directional light has units of lux, + // so to match a 1 lux light, we set a single pixel with a value equal to 1 divided by the solid + // angle of the pixel in steradians. This image is 1024 x 512, + // so the value is 1 / ( sin( phi ) * ( pi / 512 ) ^ 2 ) = 27,490 nits. + + const gui = new GUI(); + gui.add({ enabled: true }, 'enabled') + .name('PMREM') + .onChange(value => { + directionalLight.intensity = value ? 0 : 1; + + scene.traverse(function (child) { + if (child.isMesh) { + child.material.envMapIntensity = 1 - directionalLight.intensity; + } + }); + + render(); + }); +} + +function createObjects() { + let radianceMap = null; + new RGBELoader() + // .setDataType( THREE.FloatType ) + .setPath('textures/equirectangular/') + .load('spot1Lux.hdr', function (texture) { + radianceMap = pmremGenerator.fromEquirectangular(texture).texture; + pmremGenerator.dispose(); + + scene.background = radianceMap; + + const geometry = new THREE.SphereGeometry(0.4, 32, 32); + + for (let x = 0; x <= 10; x++) { + for (let y = 0; y <= 2; y++) { + const material = new THREE.MeshPhysicalMaterial({ + roughness: x / 10, + metalness: y < 1 ? 1 : 0, + color: y < 2 ? 0xffffff : 0x000000, + envMap: radianceMap, + envMapIntensity: 1, + }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.x = x - 5; + mesh.position.y = 1 - y; + scene.add(mesh); + } + } + + render(); + }); + + const pmremGenerator = new THREE.PMREMGenerator(renderer); + pmremGenerator.compileEquirectangularShader(); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + updateCamera(); + + renderer.setSize(width, height); + + render(); +} + +function updateCamera() { + const horizontalFoV = 40; + const verticalFoV = + (2 * Math.atan(Math.tan(((horizontalFoV / 2) * Math.PI) / 180) / camera.aspect) * 180) / Math.PI; + camera.fov = verticalFoV; + camera.updateProjectionMatrix(); +} + +function render() { + renderer.render(scene, camera); +} + +Promise.resolve().then(init).then(createObjects).then(render); diff --git a/examples-testing/examples/webgl_points_billboards.ts b/examples-testing/examples/webgl_points_billboards.ts new file mode 100644 index 000000000..24d4de1a9 --- /dev/null +++ b/examples-testing/examples/webgl_points_billboards.ts @@ -0,0 +1,120 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, stats, material; +let mouseX = 0, + mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 2, 2000); + camera.position.z = 1000; + + scene = new THREE.Scene(); + scene.fog = new THREE.FogExp2(0x000000, 0.001); + + const geometry = new THREE.BufferGeometry(); + const vertices = []; + + const sprite = new THREE.TextureLoader().load('textures/sprites/disc.png'); + sprite.colorSpace = THREE.SRGBColorSpace; + + for (let i = 0; i < 10000; i++) { + const x = 2000 * Math.random() - 1000; + const y = 2000 * Math.random() - 1000; + const z = 2000 * Math.random() - 1000; + + vertices.push(x, y, z); + } + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + + material = new THREE.PointsMaterial({ + size: 35, + sizeAttenuation: true, + map: sprite, + alphaTest: 0.5, + transparent: true, + }); + material.color.setHSL(1.0, 0.3, 0.7, THREE.SRGBColorSpace); + + const particles = new THREE.Points(geometry, material); + scene.add(particles); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + const gui = new GUI(); + + gui.add(material, 'sizeAttenuation').onChange(function () { + material.needsUpdate = true; + }); + + gui.open(); + + // + + document.body.style.touchAction = 'none'; + document.body.addEventListener('pointermove', onPointerMove); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onPointerMove(event) { + if (event.isPrimary === false) return; + + mouseX = event.clientX - windowHalfX; + mouseY = event.clientY - windowHalfY; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + const time = Date.now() * 0.00005; + + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-mouseY - camera.position.y) * 0.05; + + camera.lookAt(scene.position); + + const h = ((360 * (1.0 + time)) % 360) / 360; + material.color.setHSL(h, 0.5, 0.5); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_points_sprites.ts b/examples-testing/examples/webgl_points_sprites.ts new file mode 100644 index 000000000..31b9e2ce1 --- /dev/null +++ b/examples-testing/examples/webgl_points_sprites.ts @@ -0,0 +1,167 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, stats, parameters; +let mouseX = 0, + mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +const materials = []; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 2000); + camera.position.z = 1000; + + scene = new THREE.Scene(); + scene.fog = new THREE.FogExp2(0x000000, 0.0008); + + const geometry = new THREE.BufferGeometry(); + const vertices = []; + + const textureLoader = new THREE.TextureLoader(); + + const assignSRGB = texture => { + texture.colorSpace = THREE.SRGBColorSpace; + }; + + const sprite1 = textureLoader.load('textures/sprites/snowflake1.png', assignSRGB); + const sprite2 = textureLoader.load('textures/sprites/snowflake2.png', assignSRGB); + const sprite3 = textureLoader.load('textures/sprites/snowflake3.png', assignSRGB); + const sprite4 = textureLoader.load('textures/sprites/snowflake4.png', assignSRGB); + const sprite5 = textureLoader.load('textures/sprites/snowflake5.png', assignSRGB); + + for (let i = 0; i < 10000; i++) { + const x = Math.random() * 2000 - 1000; + const y = Math.random() * 2000 - 1000; + const z = Math.random() * 2000 - 1000; + + vertices.push(x, y, z); + } + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + + parameters = [ + [[1.0, 0.2, 0.5], sprite2, 20], + [[0.95, 0.1, 0.5], sprite3, 15], + [[0.9, 0.05, 0.5], sprite1, 10], + [[0.85, 0, 0.5], sprite5, 8], + [[0.8, 0, 0.5], sprite4, 5], + ]; + + for (let i = 0; i < parameters.length; i++) { + const color = parameters[i][0]; + const sprite = parameters[i][1]; + const size = parameters[i][2]; + + materials[i] = new THREE.PointsMaterial({ + size: size, + map: sprite, + blending: THREE.AdditiveBlending, + depthTest: false, + transparent: true, + }); + materials[i].color.setHSL(color[0], color[1], color[2], THREE.SRGBColorSpace); + + const particles = new THREE.Points(geometry, materials[i]); + + particles.rotation.x = Math.random() * 6; + particles.rotation.y = Math.random() * 6; + particles.rotation.z = Math.random() * 6; + + scene.add(particles); + } + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + const gui = new GUI(); + + const params = { + texture: true, + }; + + gui.add(params, 'texture').onChange(function (value) { + for (let i = 0; i < materials.length; i++) { + materials[i].map = value === true ? parameters[i][1] : null; + materials[i].needsUpdate = true; + } + }); + + gui.open(); + + document.body.style.touchAction = 'none'; + document.body.addEventListener('pointermove', onPointerMove); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onPointerMove(event) { + if (event.isPrimary === false) return; + + mouseX = event.clientX - windowHalfX; + mouseY = event.clientY - windowHalfY; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + const time = Date.now() * 0.00005; + + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-mouseY - camera.position.y) * 0.05; + + camera.lookAt(scene.position); + + for (let i = 0; i < scene.children.length; i++) { + const object = scene.children[i]; + + if (object instanceof THREE.Points) { + object.rotation.y = time * (i < 4 ? i + 1 : -(i + 1)); + } + } + + for (let i = 0; i < materials.length; i++) { + const color = parameters[i][0]; + + const h = ((360 * (color[0] + time)) % 360) / 360; + materials[i].color.setHSL(h, color[1], color[2], THREE.SRGBColorSpace); + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_points_waves.ts b/examples-testing/examples/webgl_points_waves.ts new file mode 100644 index 000000000..91986e9e9 --- /dev/null +++ b/examples-testing/examples/webgl_points_waves.ts @@ -0,0 +1,145 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +const SEPARATION = 100, + AMOUNTX = 50, + AMOUNTY = 50; + +let container, stats; +let camera, scene, renderer; + +let particles, + count = 0; + +let mouseX = 0, + mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 1000; + + scene = new THREE.Scene(); + + // + + const numParticles = AMOUNTX * AMOUNTY; + + const positions = new Float32Array(numParticles * 3); + const scales = new Float32Array(numParticles); + + let i = 0, + j = 0; + + for (let ix = 0; ix < AMOUNTX; ix++) { + for (let iy = 0; iy < AMOUNTY; iy++) { + positions[i] = ix * SEPARATION - (AMOUNTX * SEPARATION) / 2; // x + positions[i + 1] = 0; // y + positions[i + 2] = iy * SEPARATION - (AMOUNTY * SEPARATION) / 2; // z + + scales[j] = 1; + + i += 3; + j++; + } + } + + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('scale', new THREE.BufferAttribute(scales, 1)); + + const material = new THREE.ShaderMaterial({ + uniforms: { + color: { value: new THREE.Color(0xffffff) }, + }, + vertexShader: document.getElementById('vertexshader').textContent, + fragmentShader: document.getElementById('fragmentshader').textContent, + }); + + // + + particles = new THREE.Points(geometry, material); + scene.add(particles); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + container.style.touchAction = 'none'; + container.addEventListener('pointermove', onPointerMove); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function onPointerMove(event) { + if (event.isPrimary === false) return; + + mouseX = event.clientX - windowHalfX; + mouseY = event.clientY - windowHalfY; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-mouseY - camera.position.y) * 0.05; + camera.lookAt(scene.position); + + const positions = particles.geometry.attributes.position.array; + const scales = particles.geometry.attributes.scale.array; + + let i = 0, + j = 0; + + for (let ix = 0; ix < AMOUNTX; ix++) { + for (let iy = 0; iy < AMOUNTY; iy++) { + positions[i + 1] = Math.sin((ix + count) * 0.3) * 50 + Math.sin((iy + count) * 0.5) * 50; + + scales[j] = (Math.sin((ix + count) * 0.3) + 1) * 20 + (Math.sin((iy + count) * 0.5) + 1) * 20; + + i += 3; + j++; + } + } + + particles.geometry.attributes.position.needsUpdate = true; + particles.geometry.attributes.scale.needsUpdate = true; + + renderer.render(scene, camera); + + count += 0.1; +} diff --git a/examples-testing/examples/webgl_portal.ts b/examples-testing/examples/webgl_portal.ts new file mode 100644 index 000000000..4bc59593f --- /dev/null +++ b/examples-testing/examples/webgl_portal.ts @@ -0,0 +1,218 @@ +import * as THREE from 'three'; + +import * as CameraUtils from 'three/addons/utils/CameraUtils.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer; + +let cameraControls; + +let smallSphereOne, smallSphereTwo; + +let portalCamera, + leftPortal, + rightPortal, + leftPortalTexture, + reflectedPosition, + rightPortalTexture, + bottomLeftCorner, + bottomRightCorner, + topLeftCorner; + +init(); + +function init() { + const container = document.getElementById('container'); + + // renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.localClippingEnabled = true; + renderer.toneMapping = THREE.ACESFilmicToneMapping; + container.appendChild(renderer.domElement); + + // scene + scene = new THREE.Scene(); + + // camera + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 5000); + camera.position.set(0, 75, 160); + + cameraControls = new OrbitControls(camera, renderer.domElement); + cameraControls.target.set(0, 40, 0); + cameraControls.maxDistance = 400; + cameraControls.minDistance = 10; + cameraControls.update(); + + // + + const planeGeo = new THREE.PlaneGeometry(100.1, 100.1); + + // bouncing icosphere + const portalPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0.0); + const geometry = new THREE.IcosahedronGeometry(5, 0); + const material = new THREE.MeshPhongMaterial({ + color: 0xffffff, + emissive: 0x333333, + flatShading: true, + clippingPlanes: [portalPlane], + clipShadows: true, + }); + smallSphereOne = new THREE.Mesh(geometry, material); + scene.add(smallSphereOne); + smallSphereTwo = new THREE.Mesh(geometry, material); + scene.add(smallSphereTwo); + + // portals + portalCamera = new THREE.PerspectiveCamera(45, 1.0, 0.1, 500.0); + scene.add(portalCamera); + //frustumHelper = new THREE.CameraHelper( portalCamera ); + //scene.add( frustumHelper ); + bottomLeftCorner = new THREE.Vector3(); + bottomRightCorner = new THREE.Vector3(); + topLeftCorner = new THREE.Vector3(); + reflectedPosition = new THREE.Vector3(); + + leftPortalTexture = new THREE.WebGLRenderTarget(256, 256); + leftPortal = new THREE.Mesh(planeGeo, new THREE.MeshBasicMaterial({ map: leftPortalTexture.texture })); + leftPortal.position.x = -30; + leftPortal.position.y = 20; + leftPortal.scale.set(0.35, 0.35, 0.35); + scene.add(leftPortal); + + rightPortalTexture = new THREE.WebGLRenderTarget(256, 256); + rightPortal = new THREE.Mesh(planeGeo, new THREE.MeshBasicMaterial({ map: rightPortalTexture.texture })); + rightPortal.position.x = 30; + rightPortal.position.y = 20; + rightPortal.scale.set(0.35, 0.35, 0.35); + scene.add(rightPortal); + + // walls + const planeTop = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0xffffff })); + planeTop.position.y = 100; + planeTop.rotateX(Math.PI / 2); + scene.add(planeTop); + + const planeBottom = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0xffffff })); + planeBottom.rotateX(-Math.PI / 2); + scene.add(planeBottom); + + const planeFront = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0x7f7fff })); + planeFront.position.z = 50; + planeFront.position.y = 50; + planeFront.rotateY(Math.PI); + scene.add(planeFront); + + const planeBack = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0xff7fff })); + planeBack.position.z = -50; + planeBack.position.y = 50; + //planeBack.rotateY( Math.PI ); + scene.add(planeBack); + + const planeRight = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0x00ff00 })); + planeRight.position.x = 50; + planeRight.position.y = 50; + planeRight.rotateY(-Math.PI / 2); + scene.add(planeRight); + + const planeLeft = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0xff0000 })); + planeLeft.position.x = -50; + planeLeft.position.y = 50; + planeLeft.rotateY(Math.PI / 2); + scene.add(planeLeft); + + // lights + const mainLight = new THREE.PointLight(0xe7e7e7, 2.5, 250, 0); + mainLight.position.y = 60; + scene.add(mainLight); + + const greenLight = new THREE.PointLight(0x00ff00, 0.5, 1000, 0); + greenLight.position.set(550, 50, 0); + scene.add(greenLight); + + const redLight = new THREE.PointLight(0xff0000, 0.5, 1000, 0); + redLight.position.set(-550, 50, 0); + scene.add(redLight); + + const blueLight = new THREE.PointLight(0xbbbbfe, 0.5, 1000, 0); + blueLight.position.set(0, 50, 550); + scene.add(blueLight); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function renderPortal(thisPortalMesh, otherPortalMesh, thisPortalTexture) { + // set the portal camera position to be reflected about the portal plane + thisPortalMesh.worldToLocal(reflectedPosition.copy(camera.position)); + reflectedPosition.x *= -1.0; + reflectedPosition.z *= -1.0; + otherPortalMesh.localToWorld(reflectedPosition); + portalCamera.position.copy(reflectedPosition); + + // grab the corners of the other portal + // - note: the portal is viewed backwards; flip the left/right coordinates + otherPortalMesh.localToWorld(bottomLeftCorner.set(50.05, -50.05, 0.0)); + otherPortalMesh.localToWorld(bottomRightCorner.set(-50.05, -50.05, 0.0)); + otherPortalMesh.localToWorld(topLeftCorner.set(50.05, 50.05, 0.0)); + // set the projection matrix to encompass the portal's frame + CameraUtils.frameCorners(portalCamera, bottomLeftCorner, bottomRightCorner, topLeftCorner, false); + + // render the portal + thisPortalTexture.texture.colorSpace = renderer.outputColorSpace; + renderer.setRenderTarget(thisPortalTexture); + renderer.state.buffers.depth.setMask(true); // make sure the depth buffer is writable so it can be properly cleared, see #18897 + if (renderer.autoClear === false) renderer.clear(); + thisPortalMesh.visible = false; // hide this portal from its own rendering + renderer.render(scene, portalCamera); + thisPortalMesh.visible = true; // re-enable this portal's visibility for general rendering +} + +function animate() { + // move the bouncing sphere(s) + const timerOne = Date.now() * 0.01; + const timerTwo = timerOne + Math.PI * 10.0; + + smallSphereOne.position.set( + Math.cos(timerOne * 0.1) * 30, + Math.abs(Math.cos(timerOne * 0.2)) * 20 + 5, + Math.sin(timerOne * 0.1) * 30, + ); + smallSphereOne.rotation.y = Math.PI / 2 - timerOne * 0.1; + smallSphereOne.rotation.z = timerOne * 0.8; + + smallSphereTwo.position.set( + Math.cos(timerTwo * 0.1) * 30, + Math.abs(Math.cos(timerTwo * 0.2)) * 20 + 5, + Math.sin(timerTwo * 0.1) * 30, + ); + smallSphereTwo.rotation.y = Math.PI / 2 - timerTwo * 0.1; + smallSphereTwo.rotation.z = timerTwo * 0.8; + + // save the original camera properties + const currentRenderTarget = renderer.getRenderTarget(); + const currentXrEnabled = renderer.xr.enabled; + const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate; + renderer.xr.enabled = false; // Avoid camera modification + renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows + + // render the portal effect + renderPortal(leftPortal, rightPortal, leftPortalTexture); + renderPortal(rightPortal, leftPortal, rightPortalTexture); + + // restore the original rendering properties + renderer.xr.enabled = currentXrEnabled; + renderer.shadowMap.autoUpdate = currentShadowAutoUpdate; + renderer.setRenderTarget(currentRenderTarget); + + // render the main scene + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_postprocessing.ts b/examples-testing/examples/webgl_postprocessing.ts new file mode 100644 index 000000000..ecc9b28ee --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing.ts @@ -0,0 +1,86 @@ +import * as THREE from 'three'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; + +import { RGBShiftShader } from 'three/addons/shaders/RGBShiftShader.js'; +import { DotScreenShader } from 'three/addons/shaders/DotScreenShader.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +let camera, renderer, composer; +let object; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 400; + + const scene = new THREE.Scene(); + scene.fog = new THREE.Fog(0x000000, 1, 1000); + + object = new THREE.Object3D(); + scene.add(object); + + const geometry = new THREE.SphereGeometry(1, 4, 4); + const material = new THREE.MeshPhongMaterial({ color: 0xffffff, flatShading: true }); + + for (let i = 0; i < 100; i++) { + const mesh = new THREE.Mesh(geometry, material); + mesh.position.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); + mesh.position.multiplyScalar(Math.random() * 400); + mesh.rotation.set(Math.random() * 2, Math.random() * 2, Math.random() * 2); + mesh.scale.x = mesh.scale.y = mesh.scale.z = Math.random() * 50; + object.add(mesh); + } + + scene.add(new THREE.AmbientLight(0xcccccc)); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(1, 1, 1); + scene.add(light); + + // postprocessing + + composer = new EffectComposer(renderer); + composer.addPass(new RenderPass(scene, camera)); + + const effect1 = new ShaderPass(DotScreenShader); + effect1.uniforms['scale'].value = 4; + composer.addPass(effect1); + + const effect2 = new ShaderPass(RGBShiftShader); + effect2.uniforms['amount'].value = 0.0015; + composer.addPass(effect2); + + const effect3 = new OutputPass(); + composer.addPass(effect3); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + object.rotation.x += 0.005; + object.rotation.y += 0.01; + + composer.render(); +} diff --git a/examples-testing/examples/webgl_postprocessing_advanced.ts b/examples-testing/examples/webgl_postprocessing_advanced.ts new file mode 100644 index 000000000..adaef6208 --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_advanced.ts @@ -0,0 +1,304 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; +import { BloomPass } from 'three/addons/postprocessing/BloomPass.js'; +import { FilmPass } from 'three/addons/postprocessing/FilmPass.js'; +import { DotScreenPass } from 'three/addons/postprocessing/DotScreenPass.js'; +import { MaskPass, ClearMaskPass } from 'three/addons/postprocessing/MaskPass.js'; +import { TexturePass } from 'three/addons/postprocessing/TexturePass.js'; + +import { BleachBypassShader } from 'three/addons/shaders/BleachBypassShader.js'; +import { ColorifyShader } from 'three/addons/shaders/ColorifyShader.js'; +import { HorizontalBlurShader } from 'three/addons/shaders/HorizontalBlurShader.js'; +import { VerticalBlurShader } from 'three/addons/shaders/VerticalBlurShader.js'; +import { SepiaShader } from 'three/addons/shaders/SepiaShader.js'; +import { VignetteShader } from 'three/addons/shaders/VignetteShader.js'; +import { GammaCorrectionShader } from 'three/addons/shaders/GammaCorrectionShader.js'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +let container, stats; + +let composerScene, composer1, composer2, composer3, composer4; + +let cameraOrtho, cameraPerspective, sceneModel, sceneBG, renderer, mesh, directionalLight; + +const width = window.innerWidth || 2; +const height = window.innerHeight || 2; + +let halfWidth = width / 2; +let halfHeight = height / 2; + +let quadBG, quadMask, renderScene; + +const delta = 0.01; + +init(); + +function init() { + container = document.getElementById('container'); + + // + + cameraOrtho = new THREE.OrthographicCamera(-halfWidth, halfWidth, halfHeight, -halfHeight, -10000, 10000); + cameraOrtho.position.z = 100; + + cameraPerspective = new THREE.PerspectiveCamera(50, width / height, 1, 10000); + cameraPerspective.position.z = 900; + + // + + sceneModel = new THREE.Scene(); + sceneBG = new THREE.Scene(); + + // + + directionalLight = new THREE.DirectionalLight(0xffffff, 3); + directionalLight.position.set(0, -0.1, 1).normalize(); + sceneModel.add(directionalLight); + + const loader = new GLTFLoader(); + loader.load('models/gltf/LeePerrySmith/LeePerrySmith.glb', function (gltf) { + createMesh(gltf.scene.children[0].geometry, sceneModel, 100); + }); + + // + + const diffuseMap = new THREE.TextureLoader().load('textures/cube/SwedishRoyalCastle/pz.jpg'); + diffuseMap.colorSpace = THREE.SRGBColorSpace; + + const materialColor = new THREE.MeshBasicMaterial({ + map: diffuseMap, + depthTest: false, + }); + + quadBG = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), materialColor); + quadBG.position.z = -500; + quadBG.scale.set(width, height, 1); + sceneBG.add(quadBG); + + // + + const sceneMask = new THREE.Scene(); + + quadMask = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ color: 0xffaa00 })); + quadMask.position.z = -300; + quadMask.scale.set(width / 2, height / 2, 1); + sceneMask.add(quadMask); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(width, height); + renderer.setAnimationLoop(animate); + renderer.autoClear = false; + + // + + container.appendChild(renderer.domElement); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + const shaderBleach = BleachBypassShader; + const shaderSepia = SepiaShader; + const shaderVignette = VignetteShader; + + const effectBleach = new ShaderPass(shaderBleach); + const effectSepia = new ShaderPass(shaderSepia); + const effectVignette = new ShaderPass(shaderVignette); + const gammaCorrection = new ShaderPass(GammaCorrectionShader); + + effectBleach.uniforms['opacity'].value = 0.95; + + effectSepia.uniforms['amount'].value = 0.9; + + effectVignette.uniforms['offset'].value = 0.95; + effectVignette.uniforms['darkness'].value = 1.6; + + const effectBloom = new BloomPass(0.5); + const effectFilm = new FilmPass(0.35); + const effectFilmBW = new FilmPass(0.35, true); + const effectDotScreen = new DotScreenPass(new THREE.Vector2(0, 0), 0.5, 0.8); + + const effectHBlur = new ShaderPass(HorizontalBlurShader); + const effectVBlur = new ShaderPass(VerticalBlurShader); + effectHBlur.uniforms['h'].value = 2 / (width / 2); + effectVBlur.uniforms['v'].value = 2 / (height / 2); + + const effectColorify1 = new ShaderPass(ColorifyShader); + const effectColorify2 = new ShaderPass(ColorifyShader); + effectColorify1.uniforms['color'] = new THREE.Uniform(new THREE.Color(1, 0.8, 0.8)); + effectColorify2.uniforms['color'] = new THREE.Uniform(new THREE.Color(1, 0.75, 0.5)); + + const clearMask = new ClearMaskPass(); + const renderMask = new MaskPass(sceneModel, cameraPerspective); + const renderMaskInverse = new MaskPass(sceneModel, cameraPerspective); + + renderMaskInverse.inverse = true; + + // + + const rtParameters = { + stencilBuffer: true, + }; + + const rtWidth = width / 2; + const rtHeight = height / 2; + + // + + const renderBackground = new RenderPass(sceneBG, cameraOrtho); + const renderModel = new RenderPass(sceneModel, cameraPerspective); + + renderModel.clear = false; + + composerScene = new EffectComposer(renderer, new THREE.WebGLRenderTarget(rtWidth * 2, rtHeight * 2, rtParameters)); + + composerScene.addPass(renderBackground); + composerScene.addPass(renderModel); + composerScene.addPass(renderMaskInverse); + composerScene.addPass(effectHBlur); + composerScene.addPass(effectVBlur); + composerScene.addPass(clearMask); + + // + + renderScene = new TexturePass(composerScene.renderTarget2.texture); + + // + + composer1 = new EffectComposer(renderer, new THREE.WebGLRenderTarget(rtWidth, rtHeight, rtParameters)); + + composer1.addPass(renderScene); + composer1.addPass(gammaCorrection); + composer1.addPass(effectFilmBW); + composer1.addPass(effectVignette); + + // + + composer2 = new EffectComposer(renderer, new THREE.WebGLRenderTarget(rtWidth, rtHeight, rtParameters)); + + composer2.addPass(renderScene); + composer2.addPass(gammaCorrection); + composer2.addPass(effectDotScreen); + composer2.addPass(renderMask); + composer2.addPass(effectColorify1); + composer2.addPass(clearMask); + composer2.addPass(renderMaskInverse); + composer2.addPass(effectColorify2); + composer2.addPass(clearMask); + composer2.addPass(effectVignette); + + // + + composer3 = new EffectComposer(renderer, new THREE.WebGLRenderTarget(rtWidth, rtHeight, rtParameters)); + + composer3.addPass(renderScene); + composer3.addPass(gammaCorrection); + composer3.addPass(effectSepia); + composer3.addPass(effectFilm); + composer3.addPass(effectVignette); + + // + + composer4 = new EffectComposer(renderer, new THREE.WebGLRenderTarget(rtWidth, rtHeight, rtParameters)); + + composer4.addPass(renderScene); + composer4.addPass(gammaCorrection); + composer4.addPass(effectBloom); + composer4.addPass(effectFilm); + composer4.addPass(effectBleach); + composer4.addPass(effectVignette); + + renderScene.uniforms['tDiffuse'].value = composerScene.renderTarget2.texture; + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + halfWidth = window.innerWidth / 2; + halfHeight = window.innerHeight / 2; + + cameraPerspective.aspect = window.innerWidth / window.innerHeight; + cameraPerspective.updateProjectionMatrix(); + + cameraOrtho.left = -halfWidth; + cameraOrtho.right = halfWidth; + cameraOrtho.top = halfHeight; + cameraOrtho.bottom = -halfHeight; + + cameraOrtho.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + composerScene.setSize(halfWidth * 2, halfHeight * 2); + + composer1.setSize(halfWidth, halfHeight); + composer2.setSize(halfWidth, halfHeight); + composer3.setSize(halfWidth, halfHeight); + composer4.setSize(halfWidth, halfHeight); + + renderScene.uniforms['tDiffuse'].value = composerScene.renderTarget2.texture; + + quadBG.scale.set(window.innerWidth, window.innerHeight, 1); + quadMask.scale.set(window.innerWidth / 2, window.innerHeight / 2, 1); +} + +function createMesh(geometry, scene, scale) { + const diffuseMap = new THREE.TextureLoader().load('models/gltf/LeePerrySmith/Map-COL.jpg'); + diffuseMap.colorSpace = THREE.SRGBColorSpace; + + const mat2 = new THREE.MeshPhongMaterial({ + color: 0xcbcbcb, + specular: 0x080808, + shininess: 20, + map: diffuseMap, + normalMap: new THREE.TextureLoader().load('models/gltf/LeePerrySmith/Infinite-Level_02_Tangent_SmoothUV.jpg'), + normalScale: new THREE.Vector2(0.75, 0.75), + }); + + mesh = new THREE.Mesh(geometry, mat2); + mesh.position.set(0, -50, 0); + mesh.scale.set(scale, scale, scale); + + scene.add(mesh); +} + +// + +function animate() { + stats.begin(); + render(); + stats.end(); +} + +function render() { + const time = Date.now() * 0.0004; + + if (mesh) mesh.rotation.y = -time; + + renderer.setViewport(0, 0, halfWidth, halfHeight); + composerScene.render(delta); + + renderer.setViewport(0, 0, halfWidth, halfHeight); + composer1.render(delta); + + renderer.setViewport(halfWidth, 0, halfWidth, halfHeight); + composer2.render(delta); + + renderer.setViewport(0, halfHeight, halfWidth, halfHeight); + composer3.render(delta); + + renderer.setViewport(halfWidth, halfHeight, halfWidth, halfHeight); + composer4.render(delta); +} diff --git a/examples-testing/examples/webgl_postprocessing_afterimage.ts b/examples-testing/examples/webgl_postprocessing_afterimage.ts new file mode 100644 index 000000000..508f90b89 --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_afterimage.ts @@ -0,0 +1,72 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { AfterimagePass } from 'three/addons/postprocessing/AfterimagePass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +let camera, scene, renderer, composer; +let mesh; + +let afterimagePass; + +const params = { + enable: true, +}; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 400; + + scene = new THREE.Scene(); + scene.fog = new THREE.Fog(0x000000, 1, 1000); + + const geometry = new THREE.BoxGeometry(150, 150, 150, 2, 2, 2); + const material = new THREE.MeshNormalMaterial(); + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // postprocessing + + composer = new EffectComposer(renderer); + composer.addPass(new RenderPass(scene, camera)); + + afterimagePass = new AfterimagePass(); + composer.addPass(afterimagePass); + + const outputPass = new OutputPass(); + composer.addPass(outputPass); + + window.addEventListener('resize', onWindowResize); + + const gui = new GUI({ title: 'Damp setting' }); + gui.add(afterimagePass.uniforms['damp'], 'value', 0, 1).step(0.001); + gui.add(params, 'enable'); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + mesh.rotation.x += 0.005; + mesh.rotation.y += 0.01; + + afterimagePass.enabled = params.enable; + + composer.render(); +} diff --git a/examples-testing/examples/webgl_postprocessing_backgrounds.ts b/examples-testing/examples/webgl_postprocessing_backgrounds.ts new file mode 100644 index 000000000..57a6a2dbd --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_backgrounds.ts @@ -0,0 +1,214 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { TexturePass } from 'three/addons/postprocessing/TexturePass.js'; +import { CubeTexturePass } from 'three/addons/postprocessing/CubeTexturePass.js'; +import { ClearPass } from 'three/addons/postprocessing/ClearPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let scene, renderer, composer; +let clearPass, texturePass, renderPass; +let cameraP, cubeTexturePassP; +let gui, stats; + +const params = { + clearPass: true, + clearColor: 'white', + clearAlpha: 1.0, + + texturePass: true, + texturePassOpacity: 1.0, + + cubeTexturePass: true, + cubeTexturePassOpacity: 1.0, + + renderPass: true, +}; + +init(); + +clearGui(); + +function clearGui() { + if (gui) gui.destroy(); + + gui = new GUI(); + + gui.add(params, 'clearPass'); + gui.add(params, 'clearColor', ['black', 'white', 'blue', 'green', 'red']); + gui.add(params, 'clearAlpha', 0, 1); + + gui.add(params, 'texturePass'); + gui.add(params, 'texturePassOpacity', 0, 1); + + gui.add(params, 'cubeTexturePass'); + gui.add(params, 'cubeTexturePassOpacity', 0, 1); + + gui.add(params, 'renderPass'); + + gui.open(); +} + +function init() { + const container = document.getElementById('container'); + + const width = window.innerWidth || 1; + const height = window.innerHeight || 1; + const aspect = width / height; + const devicePixelRatio = window.devicePixelRatio || 1; + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(devicePixelRatio); + renderer.setSize(width, height); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + cameraP = new THREE.PerspectiveCamera(65, aspect, 1, 10); + cameraP.position.z = 7; + + scene = new THREE.Scene(); + + const group = new THREE.Group(); + scene.add(group); + + const light = new THREE.PointLight(0xefffef, 500); + light.position.z = 10; + light.position.y = -10; + light.position.x = -10; + scene.add(light); + + const light2 = new THREE.PointLight(0xffefef, 500); + light2.position.z = 10; + light2.position.x = -10; + light2.position.y = 10; + scene.add(light2); + + const light3 = new THREE.PointLight(0xefefff, 500); + light3.position.z = 10; + light3.position.x = 10; + light3.position.y = -10; + scene.add(light3); + + const geometry = new THREE.SphereGeometry(1, 48, 24); + + const material = new THREE.MeshStandardMaterial(); + material.roughness = 0.5 * Math.random() + 0.25; + material.metalness = 0; + material.color.setHSL(Math.random(), 1.0, 0.3); + + const mesh = new THREE.Mesh(geometry, material); + group.add(mesh); + + // postprocessing + + const genCubeUrls = function (prefix, postfix) { + return [ + prefix + 'px' + postfix, + prefix + 'nx' + postfix, + prefix + 'py' + postfix, + prefix + 'ny' + postfix, + prefix + 'pz' + postfix, + prefix + 'nz' + postfix, + ]; + }; + + composer = new EffectComposer(renderer); + + clearPass = new ClearPass(params.clearColor, params.clearAlpha); + composer.addPass(clearPass); + + texturePass = new TexturePass(); + composer.addPass(texturePass); + + const textureLoader = new THREE.TextureLoader(); + textureLoader.load('textures/hardwood2_diffuse.jpg', function (map) { + map.colorSpace = THREE.SRGBColorSpace; + texturePass.map = map; + }); + + cubeTexturePassP = null; + + const ldrUrls = genCubeUrls('textures/cube/pisa/', '.png'); + new THREE.CubeTextureLoader().load(ldrUrls, function (ldrCubeMap) { + cubeTexturePassP = new CubeTexturePass(cameraP, ldrCubeMap); + composer.insertPass(cubeTexturePassP, 2); + }); + + renderPass = new RenderPass(scene, cameraP); + renderPass.clear = false; + composer.addPass(renderPass); + + const outputPass = new OutputPass(); + composer.addPass(outputPass); + + const controls = new OrbitControls(cameraP, renderer.domElement); + controls.enableZoom = false; + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + const aspect = width / height; + + cameraP.aspect = aspect; + cameraP.updateProjectionMatrix(); + + renderer.setSize(width, height); + composer.setSize(width, height); +} + +function animate() { + stats.begin(); + + cameraP.updateMatrixWorld(true); + + let newColor = clearPass.clearColor; + + switch (params.clearColor) { + case 'blue': + newColor = 0x0000ff; + break; + case 'red': + newColor = 0xff0000; + break; + case 'green': + newColor = 0x00ff00; + break; + case 'white': + newColor = 0xffffff; + break; + case 'black': + newColor = 0x000000; + break; + } + + clearPass.enabled = params.clearPass; + clearPass.clearColor = newColor; + clearPass.clearAlpha = params.clearAlpha; + + texturePass.enabled = params.texturePass; + texturePass.opacity = params.texturePassOpacity; + + if (cubeTexturePassP !== null) { + cubeTexturePassP.enabled = params.cubeTexturePass; + cubeTexturePassP.opacity = params.cubeTexturePassOpacity; + } + + renderPass.enabled = params.renderPass; + + composer.render(); + + stats.end(); +} diff --git a/examples-testing/examples/webgl_postprocessing_fxaa.ts b/examples-testing/examples/webgl_postprocessing_fxaa.ts new file mode 100644 index 000000000..55745f88e --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_fxaa.ts @@ -0,0 +1,132 @@ +import * as THREE from 'three'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; +import { FXAAShader } from 'three/addons/shaders/FXAAShader.js'; + +let camera, scene, renderer, clock, group, container; + +let composer1, composer2, fxaaPass; + +init(); + +function init() { + container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(45, container.offsetWidth / container.offsetHeight, 1, 2000); + camera.position.z = 500; + + scene = new THREE.Scene(); + + clock = new THREE.Clock(); + + // + + const hemiLight = new THREE.HemisphereLight(0xffffff, 0x8d8d8d); + hemiLight.position.set(0, 1000, 0); + scene.add(hemiLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.position.set(-3000, 1000, -1000); + scene.add(dirLight); + + // + + group = new THREE.Group(); + + const geometry = new THREE.TetrahedronGeometry(10); + const material = new THREE.MeshStandardMaterial({ color: 0xf73232, flatShading: true }); + + for (let i = 0; i < 100; i++) { + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.x = Math.random() * 500 - 250; + mesh.position.y = Math.random() * 500 - 250; + mesh.position.z = Math.random() * 500 - 250; + + mesh.scale.setScalar(Math.random() * 2 + 1); + + mesh.rotation.x = Math.random() * Math.PI; + mesh.rotation.y = Math.random() * Math.PI; + mesh.rotation.z = Math.random() * Math.PI; + + group.add(mesh); + } + + scene.add(group); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(container.offsetWidth, container.offsetHeight); + renderer.setAnimationLoop(animate); + renderer.autoClear = false; + container.appendChild(renderer.domElement); + + // + + const renderPass = new RenderPass(scene, camera); + renderPass.clearAlpha = 0; + + // + + fxaaPass = new ShaderPass(FXAAShader); + + const outputPass = new OutputPass(); + + composer1 = new EffectComposer(renderer); + composer1.addPass(renderPass); + composer1.addPass(outputPass); + + // + + const pixelRatio = renderer.getPixelRatio(); + + fxaaPass.material.uniforms['resolution'].value.x = 1 / (container.offsetWidth * pixelRatio); + fxaaPass.material.uniforms['resolution'].value.y = 1 / (container.offsetHeight * pixelRatio); + + composer2 = new EffectComposer(renderer); + composer2.addPass(renderPass); + composer2.addPass(outputPass); + + // FXAA is engineered to be applied towards the end of engine post processing after conversion to low dynamic range and conversion to the sRGB color space for display. + + composer2.addPass(fxaaPass); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = container.offsetWidth / container.offsetHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(container.offsetWidth, container.offsetHeight); + composer1.setSize(container.offsetWidth, container.offsetHeight); + composer2.setSize(container.offsetWidth, container.offsetHeight); + + const pixelRatio = renderer.getPixelRatio(); + + fxaaPass.material.uniforms['resolution'].value.x = 1 / (container.offsetWidth * pixelRatio); + fxaaPass.material.uniforms['resolution'].value.y = 1 / (container.offsetHeight * pixelRatio); +} + +function animate() { + const halfWidth = container.offsetWidth / 2; + + group.rotation.y += clock.getDelta() * 0.1; + + renderer.setScissorTest(true); + + renderer.setScissor(0, 0, halfWidth - 1, container.offsetHeight); + composer1.render(); + + renderer.setScissor(halfWidth, 0, halfWidth, container.offsetHeight); + composer2.render(); + + renderer.setScissorTest(false); +} diff --git a/examples-testing/examples/webgl_postprocessing_glitch.ts b/examples-testing/examples/webgl_postprocessing_glitch.ts new file mode 100644 index 000000000..f846c0ce6 --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_glitch.ts @@ -0,0 +1,97 @@ +import * as THREE from 'three'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { GlitchPass } from 'three/addons/postprocessing/GlitchPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +let camera, scene, renderer, composer; +let object, light; + +let glitchPass; + +const button = document.querySelector('#startButton'); +button.addEventListener('click', function () { + const overlay = document.getElementById('overlay'); + overlay.remove(); + + init(); +}); + +function updateOptions() { + const wildGlitch = document.getElementById('wildGlitch'); + glitchPass.goWild = wildGlitch.checked; +} + +function init() { + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 400; + + scene = new THREE.Scene(); + scene.fog = new THREE.Fog(0x000000, 1, 1000); + + object = new THREE.Object3D(); + scene.add(object); + + const geometry = new THREE.SphereGeometry(1, 4, 4); + + for (let i = 0; i < 100; i++) { + const material = new THREE.MeshPhongMaterial({ color: 0xffffff * Math.random(), flatShading: true }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); + mesh.position.multiplyScalar(Math.random() * 400); + mesh.rotation.set(Math.random() * 2, Math.random() * 2, Math.random() * 2); + mesh.scale.x = mesh.scale.y = mesh.scale.z = Math.random() * 50; + object.add(mesh); + } + + scene.add(new THREE.AmbientLight(0xcccccc)); + + light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(1, 1, 1); + scene.add(light); + + // postprocessing + + composer = new EffectComposer(renderer); + composer.addPass(new RenderPass(scene, camera)); + + glitchPass = new GlitchPass(); + composer.addPass(glitchPass); + + const outputPass = new OutputPass(); + composer.addPass(outputPass); + + // + + window.addEventListener('resize', onWindowResize); + + const wildGlitchOption = document.getElementById('wildGlitch'); + wildGlitchOption.addEventListener('change', updateOptions); + + updateOptions(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + object.rotation.x += 0.005; + object.rotation.y += 0.01; + + composer.render(); +} diff --git a/examples-testing/examples/webgl_postprocessing_godrays.ts b/examples-testing/examples/webgl_postprocessing_godrays.ts new file mode 100644 index 000000000..fb7604411 --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_godrays.ts @@ -0,0 +1,347 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { + GodRaysFakeSunShader, + GodRaysDepthMaskShader, + GodRaysCombineShader, + GodRaysGenerateShader, +} from 'three/addons/shaders/GodRaysShader.js'; + +let container, stats; +let camera, scene, renderer, materialDepth; + +let sphereMesh; + +const sunPosition = new THREE.Vector3(0, 1000, -1000); +const clipPosition = new THREE.Vector4(); +const screenSpacePosition = new THREE.Vector3(); + +const postprocessing = { enabled: true }; + +const orbitRadius = 200; + +const bgColor = 0x000511; +const sunColor = 0xffee00; + +// Use a smaller size for some of the god-ray render targets for better performance. +const godrayRenderTargetResolutionMultiplier = 1.0 / 4.0; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + // + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 3000); + camera.position.z = 200; + + scene = new THREE.Scene(); + + // + + materialDepth = new THREE.MeshDepthMaterial(); + + // tree + + const loader = new OBJLoader(); + loader.load('models/obj/tree.obj', function (object) { + object.position.set(0, -150, -150); + object.scale.multiplyScalar(400); + scene.add(object); + }); + + // sphere + + const geo = new THREE.SphereGeometry(1, 20, 10); + sphereMesh = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({ color: 0x000000 })); + sphereMesh.scale.multiplyScalar(20); + scene.add(sphereMesh); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setClearColor(0xffffff); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + renderer.autoClear = false; + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 50; + controls.maxDistance = 500; + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); + + // + + initPostprocessing(window.innerWidth, window.innerHeight); +} + +// + +function onWindowResize() { + const renderTargetWidth = window.innerWidth; + const renderTargetHeight = window.innerHeight; + + camera.aspect = renderTargetWidth / renderTargetHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(renderTargetWidth, renderTargetHeight); + postprocessing.rtTextureColors.setSize(renderTargetWidth, renderTargetHeight); + postprocessing.rtTextureDepth.setSize(renderTargetWidth, renderTargetHeight); + postprocessing.rtTextureDepthMask.setSize(renderTargetWidth, renderTargetHeight); + + const adjustedWidth = renderTargetWidth * godrayRenderTargetResolutionMultiplier; + const adjustedHeight = renderTargetHeight * godrayRenderTargetResolutionMultiplier; + postprocessing.rtTextureGodRays1.setSize(adjustedWidth, adjustedHeight); + postprocessing.rtTextureGodRays2.setSize(adjustedWidth, adjustedHeight); +} + +function initPostprocessing(renderTargetWidth, renderTargetHeight) { + postprocessing.scene = new THREE.Scene(); + + postprocessing.camera = new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5, -10000, 10000); + postprocessing.camera.position.z = 100; + + postprocessing.scene.add(postprocessing.camera); + + postprocessing.rtTextureColors = new THREE.WebGLRenderTarget(renderTargetWidth, renderTargetHeight, { + type: THREE.HalfFloatType, + }); + + // Switching the depth formats to luminance from rgb doesn't seem to work. I didn't + // investigate further for now. + // pars.format = LuminanceFormat; + + // I would have this quarter size and use it as one of the ping-pong render + // targets but the aliasing causes some temporal flickering + + postprocessing.rtTextureDepth = new THREE.WebGLRenderTarget(renderTargetWidth, renderTargetHeight, { + type: THREE.HalfFloatType, + }); + postprocessing.rtTextureDepthMask = new THREE.WebGLRenderTarget(renderTargetWidth, renderTargetHeight, { + type: THREE.HalfFloatType, + }); + + // The ping-pong render targets can use an adjusted resolution to minimize cost + + const adjustedWidth = renderTargetWidth * godrayRenderTargetResolutionMultiplier; + const adjustedHeight = renderTargetHeight * godrayRenderTargetResolutionMultiplier; + postprocessing.rtTextureGodRays1 = new THREE.WebGLRenderTarget(adjustedWidth, adjustedHeight, { + type: THREE.HalfFloatType, + }); + postprocessing.rtTextureGodRays2 = new THREE.WebGLRenderTarget(adjustedWidth, adjustedHeight, { + type: THREE.HalfFloatType, + }); + + // god-ray shaders + + const godraysMaskShader = GodRaysDepthMaskShader; + postprocessing.godrayMaskUniforms = THREE.UniformsUtils.clone(godraysMaskShader.uniforms); + postprocessing.materialGodraysDepthMask = new THREE.ShaderMaterial({ + uniforms: postprocessing.godrayMaskUniforms, + vertexShader: godraysMaskShader.vertexShader, + fragmentShader: godraysMaskShader.fragmentShader, + }); + + const godraysGenShader = GodRaysGenerateShader; + postprocessing.godrayGenUniforms = THREE.UniformsUtils.clone(godraysGenShader.uniforms); + postprocessing.materialGodraysGenerate = new THREE.ShaderMaterial({ + uniforms: postprocessing.godrayGenUniforms, + vertexShader: godraysGenShader.vertexShader, + fragmentShader: godraysGenShader.fragmentShader, + }); + + const godraysCombineShader = GodRaysCombineShader; + postprocessing.godrayCombineUniforms = THREE.UniformsUtils.clone(godraysCombineShader.uniforms); + postprocessing.materialGodraysCombine = new THREE.ShaderMaterial({ + uniforms: postprocessing.godrayCombineUniforms, + vertexShader: godraysCombineShader.vertexShader, + fragmentShader: godraysCombineShader.fragmentShader, + }); + + const godraysFakeSunShader = GodRaysFakeSunShader; + postprocessing.godraysFakeSunUniforms = THREE.UniformsUtils.clone(godraysFakeSunShader.uniforms); + postprocessing.materialGodraysFakeSun = new THREE.ShaderMaterial({ + uniforms: postprocessing.godraysFakeSunUniforms, + vertexShader: godraysFakeSunShader.vertexShader, + fragmentShader: godraysFakeSunShader.fragmentShader, + }); + + postprocessing.godraysFakeSunUniforms.bgColor.value.setHex(bgColor); + postprocessing.godraysFakeSunUniforms.sunColor.value.setHex(sunColor); + + postprocessing.godrayCombineUniforms.fGodRayIntensity.value = 0.75; + + postprocessing.quad = new THREE.Mesh(new THREE.PlaneGeometry(1.0, 1.0), postprocessing.materialGodraysGenerate); + postprocessing.quad.position.z = -9900; + postprocessing.scene.add(postprocessing.quad); +} + +function animate() { + stats.begin(); + render(); + stats.end(); +} + +function getStepSize(filterLen, tapsPerPass, pass) { + return filterLen * Math.pow(tapsPerPass, -pass); +} + +function filterGodRays(inputTex, renderTarget, stepSize) { + postprocessing.scene.overrideMaterial = postprocessing.materialGodraysGenerate; + + postprocessing.godrayGenUniforms['fStepSize'].value = stepSize; + postprocessing.godrayGenUniforms['tInput'].value = inputTex; + + renderer.setRenderTarget(renderTarget); + renderer.render(postprocessing.scene, postprocessing.camera); + postprocessing.scene.overrideMaterial = null; +} + +function render() { + const time = Date.now() / 4000; + + sphereMesh.position.x = orbitRadius * Math.cos(time); + sphereMesh.position.z = orbitRadius * Math.sin(time) - 100; + + if (postprocessing.enabled) { + clipPosition.x = sunPosition.x; + clipPosition.y = sunPosition.y; + clipPosition.z = sunPosition.z; + clipPosition.w = 1; + + clipPosition.applyMatrix4(camera.matrixWorldInverse).applyMatrix4(camera.projectionMatrix); + + // perspective divide (produce NDC space) + + clipPosition.x /= clipPosition.w; + clipPosition.y /= clipPosition.w; + + screenSpacePosition.x = (clipPosition.x + 1) / 2; // transform from [-1,1] to [0,1] + screenSpacePosition.y = (clipPosition.y + 1) / 2; // transform from [-1,1] to [0,1] + screenSpacePosition.z = clipPosition.z; // needs to stay in clip space for visibilty checks + + // Give it to the god-ray and sun shaders + + postprocessing.godrayGenUniforms['vSunPositionScreenSpace'].value.copy(screenSpacePosition); + postprocessing.godraysFakeSunUniforms['vSunPositionScreenSpace'].value.copy(screenSpacePosition); + + // -- Draw sky and sun -- + + // Clear colors and depths, will clear to sky color + + renderer.setRenderTarget(postprocessing.rtTextureColors); + renderer.clear(true, true, false); + + // Sun render. Runs a shader that gives a brightness based on the screen + // space distance to the sun. Not very efficient, so i make a scissor + // rectangle around the suns position to avoid rendering surrounding pixels. + + const sunsqH = 0.74 * window.innerHeight; // 0.74 depends on extent of sun from shader + const sunsqW = 0.74 * window.innerHeight; // both depend on height because sun is aspect-corrected + + screenSpacePosition.x *= window.innerWidth; + screenSpacePosition.y *= window.innerHeight; + + renderer.setScissor(screenSpacePosition.x - sunsqW / 2, screenSpacePosition.y - sunsqH / 2, sunsqW, sunsqH); + renderer.setScissorTest(true); + + postprocessing.godraysFakeSunUniforms['fAspect'].value = window.innerWidth / window.innerHeight; + + postprocessing.scene.overrideMaterial = postprocessing.materialGodraysFakeSun; + renderer.setRenderTarget(postprocessing.rtTextureColors); + renderer.render(postprocessing.scene, postprocessing.camera); + + renderer.setScissorTest(false); + + // -- Draw scene objects -- + + // Colors + + scene.overrideMaterial = null; + renderer.setRenderTarget(postprocessing.rtTextureColors); + renderer.render(scene, camera); + + // Depth + + scene.overrideMaterial = materialDepth; + renderer.setRenderTarget(postprocessing.rtTextureDepth); + renderer.clear(); + renderer.render(scene, camera); + + // + + postprocessing.godrayMaskUniforms['tInput'].value = postprocessing.rtTextureDepth.texture; + + postprocessing.scene.overrideMaterial = postprocessing.materialGodraysDepthMask; + renderer.setRenderTarget(postprocessing.rtTextureDepthMask); + renderer.render(postprocessing.scene, postprocessing.camera); + + // -- Render god-rays -- + + // Maximum length of god-rays (in texture space [0,1]X[0,1]) + + const filterLen = 1.0; + + // Samples taken by filter + + const TAPS_PER_PASS = 6.0; + + // Pass order could equivalently be 3,2,1 (instead of 1,2,3), which + // would start with a small filter support and grow to large. however + // the large-to-small order produces less objectionable aliasing artifacts that + // appear as a glimmer along the length of the beams + + // pass 1 - render into first ping-pong target + filterGodRays( + postprocessing.rtTextureDepthMask.texture, + postprocessing.rtTextureGodRays2, + getStepSize(filterLen, TAPS_PER_PASS, 1.0), + ); + + // pass 2 - render into second ping-pong target + filterGodRays( + postprocessing.rtTextureGodRays2.texture, + postprocessing.rtTextureGodRays1, + getStepSize(filterLen, TAPS_PER_PASS, 2.0), + ); + + // pass 3 - 1st RT + filterGodRays( + postprocessing.rtTextureGodRays1.texture, + postprocessing.rtTextureGodRays2, + getStepSize(filterLen, TAPS_PER_PASS, 3.0), + ); + + // final pass - composite god-rays onto colors + + postprocessing.godrayCombineUniforms['tColors'].value = postprocessing.rtTextureColors.texture; + postprocessing.godrayCombineUniforms['tGodRays'].value = postprocessing.rtTextureGodRays2.texture; + + postprocessing.scene.overrideMaterial = postprocessing.materialGodraysCombine; + + renderer.setRenderTarget(null); + renderer.render(postprocessing.scene, postprocessing.camera); + postprocessing.scene.overrideMaterial = null; + } else { + renderer.setRenderTarget(null); + renderer.clear(); + renderer.render(scene, camera); + } +} diff --git a/examples-testing/examples/webgl_postprocessing_gtao.ts b/examples-testing/examples/webgl_postprocessing_gtao.ts new file mode 100644 index 000000000..4f16d1554 --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_gtao.ts @@ -0,0 +1,215 @@ +import * as THREE from 'three'; +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { GTAOPass } from 'three/addons/postprocessing/GTAOPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +let camera, scene, renderer, composer, controls, clock, stats, mixer; + +init(); + +function init() { + const dracoLoader = new DRACOLoader(); + dracoLoader.setDecoderPath('jsm/libs/draco/'); + dracoLoader.setDecoderConfig({ type: 'js' }); + const loader = new GLTFLoader(); + loader.setDRACOLoader(dracoLoader); + loader.setPath('models/gltf/'); + + clock = new THREE.Clock(); + const container = document.createElement('div'); + document.body.appendChild(container); + + stats = new Stats(); + container.appendChild(stats.dom); + + renderer = new THREE.WebGLRenderer(); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + const pmremGenerator = new THREE.PMREMGenerator(renderer); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xbfe3dd); + scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture; + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 100); + camera.position.set(5, 2, 8); + + controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 0.5, 0); + controls.update(); + controls.enablePan = false; + controls.enableDamping = true; + + const width = window.innerWidth; + const height = window.innerHeight; + + composer = new EffectComposer(renderer); + + const renderPass = new RenderPass(scene, camera); + composer.addPass(renderPass); + + const gtaoPass = new GTAOPass(scene, camera, width, height); + gtaoPass.output = GTAOPass.OUTPUT.Denoise; + composer.addPass(gtaoPass); + + const outputPass = new OutputPass(); + composer.addPass(outputPass); + + // + + loader.load( + 'LittlestTokyo.glb', + gltf => { + const model = gltf.scene; + model.position.set(1, 1, 0); + model.scale.set(0.01, 0.01, 0.01); + scene.add(model); + + mixer = new THREE.AnimationMixer(model); + mixer.clipAction(gltf.animations[0]).play(); + + const box = new THREE.Box3().setFromObject(scene); + gtaoPass.setSceneClipBox(box); + }, + undefined, + e => console.error(e), + ); + + // Init gui + const gui = new GUI(); + + gui.add(gtaoPass, 'output', { + Default: GTAOPass.OUTPUT.Default, + Diffuse: GTAOPass.OUTPUT.Diffuse, + 'AO Only': GTAOPass.OUTPUT.AO, + 'AO Only + Denoise': GTAOPass.OUTPUT.Denoise, + Depth: GTAOPass.OUTPUT.Depth, + Normal: GTAOPass.OUTPUT.Normal, + }).onChange(function (value) { + gtaoPass.output = value; + }); + + const aoParameters = { + radius: 0.25, + distanceExponent: 1, + thickness: 1, + scale: 1, + samples: 16, + distanceFallOff: 1, + screenSpaceRadius: false, + }; + const pdParameters = { + lumaPhi: 10, + depthPhi: 2, + normalPhi: 3, + radius: 4, + radiusExponent: 1, + rings: 2, + samples: 16, + }; + gtaoPass.updateGtaoMaterial(aoParameters); + gtaoPass.updatePdMaterial(pdParameters); + gui.add(gtaoPass, 'blendIntensity').min(0).max(1).step(0.01); + gui.add(aoParameters, 'radius') + .min(0.01) + .max(1) + .step(0.01) + .onChange(() => gtaoPass.updateGtaoMaterial(aoParameters)); + gui.add(aoParameters, 'distanceExponent') + .min(1) + .max(4) + .step(0.01) + .onChange(() => gtaoPass.updateGtaoMaterial(aoParameters)); + gui.add(aoParameters, 'thickness') + .min(0.01) + .max(10) + .step(0.01) + .onChange(() => gtaoPass.updateGtaoMaterial(aoParameters)); + gui.add(aoParameters, 'distanceFallOff') + .min(0) + .max(1) + .step(0.01) + .onChange(() => gtaoPass.updateGtaoMaterial(aoParameters)); + gui.add(aoParameters, 'scale') + .min(0.01) + .max(2.0) + .step(0.01) + .onChange(() => gtaoPass.updateGtaoMaterial(aoParameters)); + gui.add(aoParameters, 'samples') + .min(2) + .max(32) + .step(1) + .onChange(() => gtaoPass.updateGtaoMaterial(aoParameters)); + gui.add(aoParameters, 'screenSpaceRadius').onChange(() => gtaoPass.updateGtaoMaterial(aoParameters)); + gui.add(pdParameters, 'lumaPhi') + .min(0) + .max(20) + .step(0.01) + .onChange(() => gtaoPass.updatePdMaterial(pdParameters)); + gui.add(pdParameters, 'depthPhi') + .min(0.01) + .max(20) + .step(0.01) + .onChange(() => gtaoPass.updatePdMaterial(pdParameters)); + gui.add(pdParameters, 'normalPhi') + .min(0.01) + .max(20) + .step(0.01) + .onChange(() => gtaoPass.updatePdMaterial(pdParameters)); + gui.add(pdParameters, 'radius') + .min(0) + .max(32) + .step(1) + .onChange(() => gtaoPass.updatePdMaterial(pdParameters)); + gui.add(pdParameters, 'radiusExponent') + .min(0.1) + .max(4) + .step(0.1) + .onChange(() => gtaoPass.updatePdMaterial(pdParameters)); + gui.add(pdParameters, 'rings') + .min(1) + .max(16) + .step(0.125) + .onChange(() => gtaoPass.updatePdMaterial(pdParameters)); + gui.add(pdParameters, 'samples') + .min(2) + .max(32) + .step(1) + .onChange(() => gtaoPass.updatePdMaterial(pdParameters)); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + composer.setSize(width, height); +} + +function animate() { + const delta = clock.getDelta(); + + if (mixer) { + mixer.update(delta); + } + + controls.update(); + + stats.begin(); + composer.render(); + stats.end(); +} diff --git a/examples-testing/examples/webgl_postprocessing_masking.ts b/examples-testing/examples/webgl_postprocessing_masking.ts new file mode 100644 index 000000000..f6e7310bf --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_masking.ts @@ -0,0 +1,100 @@ +import * as THREE from 'three'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { TexturePass } from 'three/addons/postprocessing/TexturePass.js'; +import { ClearPass } from 'three/addons/postprocessing/ClearPass.js'; +import { MaskPass, ClearMaskPass } from 'three/addons/postprocessing/MaskPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +let camera, composer, renderer; +let box, torus; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 10; + + const scene1 = new THREE.Scene(); + const scene2 = new THREE.Scene(); + + box = new THREE.Mesh(new THREE.BoxGeometry(4, 4, 4)); + scene1.add(box); + + torus = new THREE.Mesh(new THREE.TorusGeometry(3, 1, 16, 32)); + scene2.add(torus); + + renderer = new THREE.WebGLRenderer(); + renderer.setClearColor(0xe0e0e0); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.autoClear = false; + document.body.appendChild(renderer.domElement); + + // + + const clearPass = new ClearPass(); + + const clearMaskPass = new ClearMaskPass(); + + const maskPass1 = new MaskPass(scene1, camera); + const maskPass2 = new MaskPass(scene2, camera); + + const texture1 = new THREE.TextureLoader().load('textures/758px-Canestra_di_frutta_(Caravaggio).jpg'); + texture1.colorSpace = THREE.SRGBColorSpace; + texture1.minFilter = THREE.LinearFilter; + const texture2 = new THREE.TextureLoader().load('textures/2294472375_24a3b8ef46_o.jpg'); + texture2.colorSpace = THREE.SRGBColorSpace; + + const texturePass1 = new TexturePass(texture1); + const texturePass2 = new TexturePass(texture2); + + const outputPass = new OutputPass(); + + const parameters = { + stencilBuffer: true, + }; + + const renderTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, parameters); + + composer = new EffectComposer(renderer, renderTarget); + composer.addPass(clearPass); + composer.addPass(maskPass1); + composer.addPass(texturePass1); + composer.addPass(clearMaskPass); + composer.addPass(maskPass2); + composer.addPass(texturePass2); + composer.addPass(clearMaskPass); + composer.addPass(outputPass); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + composer.setSize(width, height); +} + +function animate() { + const time = performance.now() * 0.001 + 6000; + + box.position.x = Math.cos(time / 1.5) * 2; + box.position.y = Math.sin(time) * 2; + box.rotation.x = time; + box.rotation.y = time / 2; + + torus.position.x = Math.cos(time) * 2; + torus.position.y = Math.sin(time / 1.5) * 2; + torus.rotation.x = time; + torus.rotation.y = time / 2; + + renderer.clear(); + composer.render(time); +} diff --git a/examples-testing/examples/webgl_postprocessing_material_ao.ts b/examples-testing/examples/webgl_postprocessing_material_ao.ts new file mode 100644 index 000000000..2f17a5304 --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_material_ao.ts @@ -0,0 +1,277 @@ +import * as THREE from 'three'; +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; +import { PLYLoader } from 'three/addons/loaders/PLYLoader.js'; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { GTAOPass } from 'three/addons/postprocessing/GTAOPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; +import { MeshPostProcessingMaterial } from 'three/addons/materials/MeshPostProcessingMaterial.js'; + +let renderer, camera, scene, composer, controls, stats; +const sceneParameters = { + output: 0, + envMapIntensity: 1.0, + ambientLightIntensity: 0.0, + lightIntensity: 50, + shadow: true, +}; +const aoParameters = { + radius: 0.5, + distanceExponent: 2, + thickness: 10, + scale: 1, + samples: 16, + distanceFallOff: 1, +}; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + stats = new Stats(); + container.appendChild(stats.dom); + + renderer = new THREE.WebGLRenderer(); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + renderer.shadowMap.enabled = sceneParameters.shadow; + + const plyLoader = new PLYLoader(); + const rgbeloader = new RGBELoader(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 50); + camera.position.set(0, 3, 5); + controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 1, 0); + controls.update(); + controls.enablePan = false; + controls.enableDamping = true; + + const width = window.innerWidth; + const height = window.innerHeight; + + scene = new THREE.Scene(); + composer = new EffectComposer(renderer); + + const gtaoPass = new GTAOPass(scene, camera, width, height); + gtaoPass.output = GTAOPass.OUTPUT.Off; + const renderPasse = new RenderPass(scene, camera); + const outputPass = new OutputPass(); + + composer.addPass(gtaoPass); + composer.addPass(renderPasse); + composer.addPass(outputPass); + + rgbeloader.load('textures/equirectangular/royal_esplanade_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + scene.environment = texture; + }); + + const groundMaterial = new MeshPostProcessingMaterial({ + color: 0x7f7f7f, + envMapIntensity: sceneParameters.envMapIntensity, + aoPassMap: gtaoPass.gtaoMap, + }); + const objectMaterial = new MeshPostProcessingMaterial({ + color: 0xffffff, + roughness: 0.5, + metalness: 0.5, + envMapIntensity: sceneParameters.envMapIntensity, + aoPassMap: gtaoPass.gtaoMap, + }); + const emissiveMaterial = new MeshPostProcessingMaterial({ + color: 0, + emissive: 0xffffff, + aoPassMap: gtaoPass.gtaoMap, + }); + plyLoader.load('models/ply/binary/Lucy100k.ply', geometry => { + geometry.computeVertexNormals(); + const lucy = new THREE.Mesh(geometry, objectMaterial); + lucy.receiveShadow = true; + lucy.castShadow = true; + lucy.scale.setScalar(0.001); + lucy.rotation.set(0, Math.PI, 0); + lucy.position.set(0.04, 1.8, 0.02); + scene.add(lucy); + }); + const ambientLight = new THREE.AmbientLight(0xffffff, sceneParameters.ambientLightIntensity); + const lightGroup = new THREE.Group(); + const planeGeometry = new THREE.PlaneGeometry(6, 6); + const cylinderGeometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 64); + const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32); + const lightSphereGeometry = new THREE.SphereGeometry(0.1, 32, 32); + scene.background = new THREE.Color(0xbfe3dd); + scene.add(ambientLight); + scene.add(lightGroup); + const targetObject = new THREE.Object3D(); + targetObject.position.set(0, 1, 0); + scene.add(targetObject); + const lightColors = [0xff4040, 0x40ff40, 0x4040ff]; + for (let j = 0; j < 3; ++j) { + const light = new THREE.SpotLight(lightColors[j], sceneParameters.lightIntensity, 0, Math.PI / 9); + light.castShadow = true; + light.shadow.camera.far = 15; + light.position.set(5 * Math.cos((Math.PI * j * 2) / 3), 2.5, 5 * Math.sin((Math.PI * j * 2) / 3)); + light.target = targetObject; + lightGroup.add(light); + } + + const groundPlane = new THREE.Mesh(planeGeometry, groundMaterial); + groundPlane.rotation.x = -Math.PI / 2; + groundPlane.position.set(0, 0, 0); + groundPlane.receiveShadow = true; + scene.add(groundPlane); + const pedestal = new THREE.Mesh(cylinderGeometry, groundMaterial); + pedestal.position.set(0, 0.5, 0); + pedestal.receiveShadow = true; + pedestal.castShadow = true; + scene.add(pedestal); + const sphereMesh = new THREE.InstancedMesh(sphereGeometry, objectMaterial, 6); + sphereMesh.receiveShadow = true; + sphereMesh.castShadow = true; + scene.add(sphereMesh); + [...Array(6).keys()].forEach(i => + sphereMesh.setMatrixAt( + i, + new THREE.Matrix4().makeTranslation(Math.cos((Math.PI * i) / 3), 0.5, Math.sin((Math.PI * i) / 3)), + ), + ); + const lightSphereMesh = new THREE.InstancedMesh(lightSphereGeometry, emissiveMaterial, 4); + scene.add(lightSphereMesh); + [...Array(4).keys()].forEach(i => + lightSphereMesh.setMatrixAt( + i, + new THREE.Matrix4().makeTranslation( + 0.4 * Math.cos((Math.PI * (i + 0.5)) / 2), + 1.1, + 0.45 * Math.sin((Math.PI * (i + 0.5)) / 2), + ), + ), + ); + + const updateGtaoMaterial = () => gtaoPass.updateGtaoMaterial(aoParameters); + const updateOutput = () => { + composer.removePass(gtaoPass); + composer.insertPass(gtaoPass, sceneParameters.output == 1 ? 1 : 0); + + switch (sceneParameters.output) { + default: + case 0: + gtaoPass.output = GTAOPass.OUTPUT.Off; + gtaoPass.enabled = true; + renderPasse.enabled = true; + break; + case 1: + gtaoPass.output = GTAOPass.OUTPUT.Default; + gtaoPass.enabled = true; + renderPasse.enabled = true; + break; + case 2: + gtaoPass.output = GTAOPass.OUTPUT.Diffuse; + gtaoPass.enabled = false; + renderPasse.enabled = true; + break; + case 3: + gtaoPass.output = GTAOPass.OUTPUT.Denoise; + gtaoPass.enabled = true; + renderPasse.enabled = false; + break; + } + + groundMaterial.aoPassMap = sceneParameters.output === 0 ? gtaoPass.gtaoMap : null; + objectMaterial.aoPassMap = sceneParameters.output === 0 ? gtaoPass.gtaoMap : null; + }; + + updateOutput(); + updateGtaoMaterial(); + + const gui = new GUI(); + gui.add(sceneParameters, 'output', { + 'material AO': 0, + 'post blended AO': 1, + 'only diffuse': 2, + 'only AO': 3, + }).onChange(() => updateOutput()); + gui.add(sceneParameters, 'envMapIntensity') + .min(0) + .max(1) + .step(0.01) + .onChange(() => { + groundMaterial.envMapIntensity = sceneParameters.envMapIntensity; + objectMaterial.envMapIntensity = sceneParameters.envMapIntensity; + }); + gui.add(sceneParameters, 'ambientLightIntensity') + .min(0.0) + .max(1.0) + .step(0.01) + .onChange(() => { + ambientLight.intensity = sceneParameters.ambientLightIntensity; + }); + gui.add(sceneParameters, 'lightIntensity') + .min(0) + .max(100) + .step(1) + .onChange(() => { + lightGroup.children.forEach(light => (light.intensity = sceneParameters.lightIntensity)); + }); + gui.add(sceneParameters, 'shadow').onChange(value => { + renderer.shadowMap.enabled = value; + lightGroup.children.forEach(light => (light.castShadow = value)); + }); + gui.add(aoParameters, 'radius') + .min(0.01) + .max(2) + .step(0.01) + .onChange(() => updateGtaoMaterial()); + gui.add(aoParameters, 'distanceExponent') + .min(1) + .max(4) + .step(0.01) + .onChange(() => updateGtaoMaterial()); + gui.add(aoParameters, 'thickness') + .min(0.01) + .max(10) + .step(0.01) + .onChange(() => updateGtaoMaterial()); + gui.add(aoParameters, 'distanceFallOff') + .min(0) + .max(1) + .step(0.01) + .onChange(() => updateGtaoMaterial()); + gui.add(aoParameters, 'scale') + .min(0.01) + .max(2.0) + .step(0.01) + .onChange(() => updateGtaoMaterial()); + gui.add(aoParameters, 'samples') + .min(2) + .max(32) + .step(1) + .onChange(() => updateGtaoMaterial()); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + composer.setSize(width, height); +} + +function animate() { + controls.update(); + stats.begin(); + composer.render(); + stats.end(); +} diff --git a/examples-testing/examples/webgl_postprocessing_outline.ts b/examples-testing/examples/webgl_postprocessing_outline.ts new file mode 100644 index 000000000..356575460 --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_outline.ts @@ -0,0 +1,282 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; +import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; +import { FXAAShader } from 'three/addons/shaders/FXAAShader.js'; + +let container, stats; +let camera, scene, renderer, controls; +let composer, effectFXAA, outlinePass; + +let selectedObjects = []; + +const raycaster = new THREE.Raycaster(); +const mouse = new THREE.Vector2(); + +const obj3d = new THREE.Object3D(); +const group = new THREE.Group(); + +const params = { + edgeStrength: 3.0, + edgeGlow: 0.0, + edgeThickness: 1.0, + pulsePeriod: 0, + rotate: false, + usePatternTexture: false, +}; + +// Init gui + +const gui = new GUI({ width: 280 }); + +gui.add(params, 'edgeStrength', 0.01, 10).onChange(function (value) { + outlinePass.edgeStrength = Number(value); +}); + +gui.add(params, 'edgeGlow', 0.0, 1).onChange(function (value) { + outlinePass.edgeGlow = Number(value); +}); + +gui.add(params, 'edgeThickness', 1, 4).onChange(function (value) { + outlinePass.edgeThickness = Number(value); +}); + +gui.add(params, 'pulsePeriod', 0.0, 5).onChange(function (value) { + outlinePass.pulsePeriod = Number(value); +}); + +gui.add(params, 'rotate'); + +gui.add(params, 'usePatternTexture').onChange(function (value) { + outlinePass.usePatternTexture = value; +}); + +function Configuration() { + this.visibleEdgeColor = '#ffffff'; + this.hiddenEdgeColor = '#190a05'; +} + +const conf = new Configuration(); + +gui.addColor(conf, 'visibleEdgeColor').onChange(function (value) { + outlinePass.visibleEdgeColor.set(value); +}); + +gui.addColor(conf, 'hiddenEdgeColor').onChange(function (value) { + outlinePass.hiddenEdgeColor.set(value); +}); + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + const width = window.innerWidth; + const height = window.innerHeight; + + renderer = new THREE.WebGLRenderer(); + renderer.shadowMap.enabled = true; + // todo - support pixelRatio in this demo + renderer.setSize(width, height); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 100); + camera.position.set(0, 0, 8); + + controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 5; + controls.maxDistance = 20; + controls.enablePan = false; + controls.enableDamping = true; + controls.dampingFactor = 0.05; + + // + + scene.add(new THREE.AmbientLight(0xaaaaaa, 0.6)); + + const light = new THREE.DirectionalLight(0xddffdd, 2); + light.position.set(1, 1, 1); + light.castShadow = true; + light.shadow.mapSize.width = 1024; + light.shadow.mapSize.height = 1024; + + const d = 10; + + light.shadow.camera.left = -d; + light.shadow.camera.right = d; + light.shadow.camera.top = d; + light.shadow.camera.bottom = -d; + light.shadow.camera.far = 1000; + + scene.add(light); + + // model + + const loader = new OBJLoader(); + loader.load('models/obj/tree.obj', function (object) { + let scale = 1.0; + + object.traverse(function (child) { + if (child instanceof THREE.Mesh) { + child.geometry.center(); + child.geometry.computeBoundingSphere(); + scale = 0.2 * child.geometry.boundingSphere.radius; + + const phongMaterial = new THREE.MeshPhongMaterial({ + color: 0xffffff, + specular: 0x111111, + shininess: 5, + }); + child.material = phongMaterial; + child.receiveShadow = true; + child.castShadow = true; + } + }); + + object.position.y = 1; + object.scale.divideScalar(scale); + obj3d.add(object); + }); + + scene.add(group); + + group.add(obj3d); + + // + + const geometry = new THREE.SphereGeometry(3, 48, 24); + + for (let i = 0; i < 20; i++) { + const material = new THREE.MeshLambertMaterial(); + material.color.setHSL(Math.random(), 1.0, 0.3); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.x = Math.random() * 4 - 2; + mesh.position.y = Math.random() * 4 - 2; + mesh.position.z = Math.random() * 4 - 2; + mesh.receiveShadow = true; + mesh.castShadow = true; + mesh.scale.multiplyScalar(Math.random() * 0.3 + 0.1); + group.add(mesh); + } + + const floorMaterial = new THREE.MeshLambertMaterial({ side: THREE.DoubleSide }); + + const floorGeometry = new THREE.PlaneGeometry(12, 12); + const floorMesh = new THREE.Mesh(floorGeometry, floorMaterial); + floorMesh.rotation.x -= Math.PI * 0.5; + floorMesh.position.y -= 1.5; + group.add(floorMesh); + floorMesh.receiveShadow = true; + + const torusGeometry = new THREE.TorusGeometry(1, 0.3, 16, 100); + const torusMaterial = new THREE.MeshPhongMaterial({ color: 0xffaaff }); + const torus = new THREE.Mesh(torusGeometry, torusMaterial); + torus.position.z = -4; + group.add(torus); + torus.receiveShadow = true; + torus.castShadow = true; + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // postprocessing + + composer = new EffectComposer(renderer); + + const renderPass = new RenderPass(scene, camera); + composer.addPass(renderPass); + + outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera); + composer.addPass(outlinePass); + + const textureLoader = new THREE.TextureLoader(); + textureLoader.load('textures/tri_pattern.jpg', function (texture) { + outlinePass.patternTexture = texture; + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + }); + + const outputPass = new OutputPass(); + composer.addPass(outputPass); + + effectFXAA = new ShaderPass(FXAAShader); + effectFXAA.uniforms['resolution'].value.set(1 / window.innerWidth, 1 / window.innerHeight); + composer.addPass(effectFXAA); + + window.addEventListener('resize', onWindowResize); + + renderer.domElement.style.touchAction = 'none'; + renderer.domElement.addEventListener('pointermove', onPointerMove); + + function onPointerMove(event) { + if (event.isPrimary === false) return; + + mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; + + checkIntersection(); + } + + function addSelectedObject(object) { + selectedObjects = []; + selectedObjects.push(object); + } + + function checkIntersection() { + raycaster.setFromCamera(mouse, camera); + + const intersects = raycaster.intersectObject(scene, true); + + if (intersects.length > 0) { + const selectedObject = intersects[0].object; + addSelectedObject(selectedObject); + outlinePass.selectedObjects = selectedObjects; + } else { + // outlinePass.selectedObjects = []; + } + } +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + composer.setSize(width, height); + + effectFXAA.uniforms['resolution'].value.set(1 / window.innerWidth, 1 / window.innerHeight); +} + +function animate() { + stats.begin(); + + const timer = performance.now(); + + if (params.rotate) { + group.rotation.y = timer * 0.0001; + } + + controls.update(); + + composer.render(); + + stats.end(); +} diff --git a/examples-testing/examples/webgl_postprocessing_pixel.ts b/examples-testing/examples/webgl_postprocessing_pixel.ts new file mode 100644 index 000000000..15b54d072 --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_pixel.ts @@ -0,0 +1,228 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPixelatedPass } from 'three/addons/postprocessing/RenderPixelatedPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, composer, crystalMesh, clock; +let gui, params; + +init(); + +function init() { + const aspectRatio = window.innerWidth / window.innerHeight; + + camera = new THREE.OrthographicCamera(-aspectRatio, aspectRatio, 1, -1, 0.1, 10); + camera.position.y = 2 * Math.tan(Math.PI / 6); + camera.position.z = 2; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x151729); + + clock = new THREE.Clock(); + + renderer = new THREE.WebGLRenderer(); + renderer.shadowMap.enabled = true; + //renderer.setPixelRatio( window.devicePixelRatio ); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + composer = new EffectComposer(renderer); + const renderPixelatedPass = new RenderPixelatedPass(6, scene, camera); + composer.addPass(renderPixelatedPass); + + const outputPass = new OutputPass(); + composer.addPass(outputPass); + + window.addEventListener('resize', onWindowResize); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.maxZoom = 2; + + // gui + + gui = new GUI(); + params = { pixelSize: 6, normalEdgeStrength: 0.3, depthEdgeStrength: 0.4, pixelAlignedPanning: true }; + gui.add(params, 'pixelSize') + .min(1) + .max(16) + .step(1) + .onChange(() => { + renderPixelatedPass.setPixelSize(params.pixelSize); + }); + gui.add(renderPixelatedPass, 'normalEdgeStrength').min(0).max(2).step(0.05); + gui.add(renderPixelatedPass, 'depthEdgeStrength').min(0).max(1).step(0.05); + gui.add(params, 'pixelAlignedPanning'); + + // textures + + const loader = new THREE.TextureLoader(); + const texChecker = pixelTexture(loader.load('textures/checker.png')); + const texChecker2 = pixelTexture(loader.load('textures/checker.png')); + texChecker.repeat.set(3, 3); + texChecker2.repeat.set(1.5, 1.5); + + // meshes + + const boxMaterial = new THREE.MeshPhongMaterial({ map: texChecker2 }); + + function addBox(boxSideLength, x, z, rotation) { + const mesh = new THREE.Mesh(new THREE.BoxGeometry(boxSideLength, boxSideLength, boxSideLength), boxMaterial); + mesh.castShadow = true; + mesh.receiveShadow = true; + mesh.rotation.y = rotation; + mesh.position.y = boxSideLength / 2; + mesh.position.set(x, boxSideLength / 2 + 0.0001, z); + scene.add(mesh); + return mesh; + } + + addBox(0.4, 0, 0, Math.PI / 4); + addBox(0.5, -0.5, -0.5, Math.PI / 4); + + const planeSideLength = 2; + const planeMesh = new THREE.Mesh( + new THREE.PlaneGeometry(planeSideLength, planeSideLength), + new THREE.MeshPhongMaterial({ map: texChecker }), + ); + planeMesh.receiveShadow = true; + planeMesh.rotation.x = -Math.PI / 2; + scene.add(planeMesh); + + const radius = 0.2; + const geometry = new THREE.IcosahedronGeometry(radius); + crystalMesh = new THREE.Mesh( + geometry, + new THREE.MeshPhongMaterial({ + color: 0x68b7e9, + emissive: 0x4f7e8b, + shininess: 10, + specular: 0xffffff, + }), + ); + crystalMesh.receiveShadow = true; + crystalMesh.castShadow = true; + scene.add(crystalMesh); + + // lights + + scene.add(new THREE.AmbientLight(0x757f8e, 3)); + + const directionalLight = new THREE.DirectionalLight(0xfffecd, 1.5); + directionalLight.position.set(100, 100, 100); + directionalLight.castShadow = true; + directionalLight.shadow.mapSize.set(2048, 2048); + scene.add(directionalLight); + + const spotLight = new THREE.SpotLight(0xffc100, 10, 10, Math.PI / 16, 0.02, 2); + spotLight.position.set(2, 2, 0); + const target = spotLight.target; + scene.add(target); + target.position.set(0, 0, 0); + spotLight.castShadow = true; + scene.add(spotLight); +} + +function onWindowResize() { + const aspectRatio = window.innerWidth / window.innerHeight; + camera.left = -aspectRatio; + camera.right = aspectRatio; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const t = clock.getElapsedTime(); + + crystalMesh.material.emissiveIntensity = Math.sin(t * 3) * 0.5 + 0.5; + crystalMesh.position.y = 0.7 + Math.sin(t * 2) * 0.05; + crystalMesh.rotation.y = stopGoEased(t, 2, 4) * 2 * Math.PI; + + const rendererSize = renderer.getSize(new THREE.Vector2()); + const aspectRatio = rendererSize.x / rendererSize.y; + if (params['pixelAlignedPanning']) { + pixelAlignFrustum( + camera, + aspectRatio, + Math.floor(rendererSize.x / params['pixelSize']), + Math.floor(rendererSize.y / params['pixelSize']), + ); + } else if (camera.left != -aspectRatio || camera.top != 1.0) { + // Reset the Camera Frustum if it has been modified + camera.left = -aspectRatio; + camera.right = aspectRatio; + camera.top = 1.0; + camera.bottom = -1.0; + camera.updateProjectionMatrix(); + } + + composer.render(); +} + +// Helper functions + +function pixelTexture(texture) { + texture.minFilter = THREE.NearestFilter; + texture.magFilter = THREE.NearestFilter; + texture.generateMipmaps = false; + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + texture.colorSpace = THREE.SRGBColorSpace; + return texture; +} + +function easeInOutCubic(x) { + return x ** 2 * 3 - x ** 3 * 2; +} + +function linearStep(x, edge0, edge1) { + const w = edge1 - edge0; + const m = 1 / w; + const y0 = -m * edge0; + return THREE.MathUtils.clamp(y0 + m * x, 0, 1); +} + +function stopGoEased(x, downtime, period) { + const cycle = (x / period) | 0; + const tween = x - cycle * period; + const linStep = easeInOutCubic(linearStep(tween, downtime, period)); + return cycle + linStep; +} + +function pixelAlignFrustum(camera, aspectRatio, pixelsPerScreenWidth, pixelsPerScreenHeight) { + // 0. Get Pixel Grid Units + const worldScreenWidth = (camera.right - camera.left) / camera.zoom; + const worldScreenHeight = (camera.top - camera.bottom) / camera.zoom; + const pixelWidth = worldScreenWidth / pixelsPerScreenWidth; + const pixelHeight = worldScreenHeight / pixelsPerScreenHeight; + + // 1. Project the current camera position along its local rotation bases + const camPos = new THREE.Vector3(); + camera.getWorldPosition(camPos); + const camRot = new THREE.Quaternion(); + camera.getWorldQuaternion(camRot); + const camRight = new THREE.Vector3(1.0, 0.0, 0.0).applyQuaternion(camRot); + const camUp = new THREE.Vector3(0.0, 1.0, 0.0).applyQuaternion(camRot); + const camPosRight = camPos.dot(camRight); + const camPosUp = camPos.dot(camUp); + + // 2. Find how far along its position is along these bases in pixel units + const camPosRightPx = camPosRight / pixelWidth; + const camPosUpPx = camPosUp / pixelHeight; + + // 3. Find the fractional pixel units and convert to world units + const fractX = camPosRightPx - Math.round(camPosRightPx); + const fractY = camPosUpPx - Math.round(camPosUpPx); + + // 4. Add fractional world units to the left/right top/bottom to align with the pixel grid + camera.left = -aspectRatio - fractX * pixelWidth; + camera.right = aspectRatio - fractX * pixelWidth; + camera.top = 1.0 - fractY * pixelHeight; + camera.bottom = -1.0 - fractY * pixelHeight; + camera.updateProjectionMatrix(); +} diff --git a/examples-testing/examples/webgl_postprocessing_procedural.ts b/examples-testing/examples/webgl_postprocessing_procedural.ts new file mode 100644 index 000000000..869824270 --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_procedural.ts @@ -0,0 +1,77 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let postCamera, postScene, renderer; +let postMaterial, noiseRandom1DMaterial, noiseRandom2DMaterial, noiseRandom3DMaterial, postQuad; +let stats; + +const params = { procedure: 'noiseRandom3D' }; + +init(); + +function init() { + const container = document.getElementById('container'); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + // Setup post processing stage + postCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); + noiseRandom1DMaterial = new THREE.ShaderMaterial({ + vertexShader: document.querySelector('#procedural-vert').textContent.trim(), + fragmentShader: document.querySelector('#noiseRandom1D-frag').textContent.trim(), + }); + noiseRandom2DMaterial = new THREE.ShaderMaterial({ + vertexShader: document.querySelector('#procedural-vert').textContent.trim(), + fragmentShader: document.querySelector('#noiseRandom2D-frag').textContent.trim(), + }); + noiseRandom3DMaterial = new THREE.ShaderMaterial({ + vertexShader: document.querySelector('#procedural-vert').textContent.trim(), + fragmentShader: document.querySelector('#noiseRandom3D-frag').textContent.trim(), + }); + postMaterial = noiseRandom3DMaterial; + const postPlane = new THREE.PlaneGeometry(2, 2); + postQuad = new THREE.Mesh(postPlane, postMaterial); + postScene = new THREE.Scene(); + postScene.add(postQuad); + + window.addEventListener('resize', onWindowResize); + + // + + const gui = new GUI(); + gui.add(params, 'procedure', ['noiseRandom1D', 'noiseRandom2D', 'noiseRandom3D']); +} + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + switch (params.procedure) { + case 'noiseRandom1D': + postMaterial = noiseRandom1DMaterial; + break; + case 'noiseRandom2D': + postMaterial = noiseRandom2DMaterial; + break; + case 'noiseRandom3D': + postMaterial = noiseRandom3DMaterial; + break; + } + + postQuad.material = postMaterial; + + // render post FX + renderer.render(postScene, postCamera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_postprocessing_rgb_halftone.ts b/examples-testing/examples/webgl_postprocessing_rgb_halftone.ts new file mode 100644 index 000000000..fa46d4c8d --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_rgb_halftone.ts @@ -0,0 +1,167 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { HalftonePass } from 'three/addons/postprocessing/HalftonePass.js'; + +let renderer, clock, camera, stats; + +const rotationSpeed = Math.PI / 64; + +let composer, group; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + + clock = new THREE.Clock(); + + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 12; + + stats = new Stats(); + + document.body.appendChild(renderer.domElement); + document.body.appendChild(stats.dom); + + // camera controls + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 0, 0); + controls.update(); + + // scene + + const scene = new THREE.Scene(); + scene.background = new THREE.Color(0x444444); + + group = new THREE.Group(); + const floor = new THREE.Mesh(new THREE.BoxGeometry(100, 1, 100), new THREE.MeshPhongMaterial({})); + floor.position.y = -10; + const light = new THREE.PointLight(0xffffff, 250); + light.position.y = 2; + group.add(floor, light); + scene.add(group); + + const mat = new THREE.ShaderMaterial({ + uniforms: {}, + + vertexShader: [ + 'varying vec2 vUV;', + 'varying vec3 vNormal;', + + 'void main() {', + + 'vUV = uv;', + 'vNormal = vec3( normal );', + 'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', + + '}', + ].join('\n'), + + fragmentShader: [ + 'varying vec2 vUV;', + 'varying vec3 vNormal;', + + 'void main() {', + + 'vec4 c = vec4( abs( vNormal ) + vec3( vUV, 0.0 ), 0.0 );', + 'gl_FragColor = c;', + + '}', + ].join('\n'), + }); + + for (let i = 0; i < 50; ++i) { + // fill scene with coloured cubes + const mesh = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), mat); + mesh.position.set(Math.random() * 16 - 8, Math.random() * 16 - 8, Math.random() * 16 - 8); + mesh.rotation.set(Math.random() * Math.PI * 2, Math.random() * Math.PI * 2, Math.random() * Math.PI * 2); + group.add(mesh); + } + + // post-processing + + composer = new EffectComposer(renderer); + const renderPass = new RenderPass(scene, camera); + const params = { + shape: 1, + radius: 4, + rotateR: Math.PI / 12, + rotateB: (Math.PI / 12) * 2, + rotateG: (Math.PI / 12) * 3, + scatter: 0, + blending: 1, + blendingMode: 1, + greyscale: false, + disable: false, + }; + const halftonePass = new HalftonePass(window.innerWidth, window.innerHeight, params); + composer.addPass(renderPass); + composer.addPass(halftonePass); + + window.onresize = function () { + // resize composer + renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + }; + + // GUI + + const controller = { + radius: halftonePass.uniforms['radius'].value, + rotateR: halftonePass.uniforms['rotateR'].value / (Math.PI / 180), + rotateG: halftonePass.uniforms['rotateG'].value / (Math.PI / 180), + rotateB: halftonePass.uniforms['rotateB'].value / (Math.PI / 180), + scatter: halftonePass.uniforms['scatter'].value, + shape: halftonePass.uniforms['shape'].value, + greyscale: halftonePass.uniforms['greyscale'].value, + blending: halftonePass.uniforms['blending'].value, + blendingMode: halftonePass.uniforms['blendingMode'].value, + disable: halftonePass.uniforms['disable'].value, + }; + + function onGUIChange() { + // update uniforms + halftonePass.uniforms['radius'].value = controller.radius; + halftonePass.uniforms['rotateR'].value = controller.rotateR * (Math.PI / 180); + halftonePass.uniforms['rotateG'].value = controller.rotateG * (Math.PI / 180); + halftonePass.uniforms['rotateB'].value = controller.rotateB * (Math.PI / 180); + halftonePass.uniforms['scatter'].value = controller.scatter; + halftonePass.uniforms['shape'].value = controller.shape; + halftonePass.uniforms['greyscale'].value = controller.greyscale; + halftonePass.uniforms['blending'].value = controller.blending; + halftonePass.uniforms['blendingMode'].value = controller.blendingMode; + halftonePass.uniforms['disable'].value = controller.disable; + } + + const gui = new GUI(); + gui.add(controller, 'shape', { Dot: 1, Ellipse: 2, Line: 3, Square: 4 }).onChange(onGUIChange); + gui.add(controller, 'radius', 1, 25).onChange(onGUIChange); + gui.add(controller, 'rotateR', 0, 90).onChange(onGUIChange); + gui.add(controller, 'rotateG', 0, 90).onChange(onGUIChange); + gui.add(controller, 'rotateB', 0, 90).onChange(onGUIChange); + gui.add(controller, 'scatter', 0, 1, 0.01).onChange(onGUIChange); + gui.add(controller, 'greyscale').onChange(onGUIChange); + gui.add(controller, 'blending', 0, 1, 0.01).onChange(onGUIChange); + gui.add(controller, 'blendingMode', { Linear: 1, Multiply: 2, Add: 3, Lighter: 4, Darker: 5 }).onChange( + onGUIChange, + ); + gui.add(controller, 'disable').onChange(onGUIChange); +} + +function animate() { + const delta = clock.getDelta(); + stats.update(); + group.rotation.y += delta * rotationSpeed; + composer.render(delta); +} diff --git a/examples-testing/examples/webgl_postprocessing_sao.ts b/examples-testing/examples/webgl_postprocessing_sao.ts new file mode 100644 index 000000000..bf40d026b --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_sao.ts @@ -0,0 +1,137 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { SAOPass } from 'three/addons/postprocessing/SAOPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +let container, stats; +let camera, scene, renderer; +let composer, renderPass, saoPass; +let group; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + const width = window.innerWidth; + const height = window.innerHeight; + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(width, height); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + camera = new THREE.PerspectiveCamera(65, width / height, 3, 10); + camera.position.z = 7; + + scene = new THREE.Scene(); + + group = new THREE.Object3D(); + scene.add(group); + + const light = new THREE.PointLight(0xefffef, 500); + light.position.z = 10; + light.position.y = -10; + light.position.x = -10; + scene.add(light); + + const light2 = new THREE.PointLight(0xffefef, 500); + light2.position.z = 10; + light2.position.x = -10; + light2.position.y = 10; + scene.add(light2); + + const light3 = new THREE.PointLight(0xefefff, 500); + light3.position.z = 10; + light3.position.x = 10; + light3.position.y = -10; + scene.add(light3); + + const light4 = new THREE.AmbientLight(0xffffff, 0.2); + scene.add(light4); + + const geometry = new THREE.SphereGeometry(3, 48, 24); + + for (let i = 0; i < 120; i++) { + const material = new THREE.MeshStandardMaterial(); + material.roughness = 0.5 * Math.random() + 0.25; + material.metalness = 0; + material.color.setHSL(Math.random(), 1.0, 0.3); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.x = Math.random() * 4 - 2; + mesh.position.y = Math.random() * 4 - 2; + mesh.position.z = Math.random() * 4 - 2; + mesh.rotation.x = Math.random(); + mesh.rotation.y = Math.random(); + mesh.rotation.z = Math.random(); + + mesh.scale.x = mesh.scale.y = mesh.scale.z = Math.random() * 0.2 + 0.05; + group.add(mesh); + } + + stats = new Stats(); + container.appendChild(stats.dom); + + composer = new EffectComposer(renderer); + renderPass = new RenderPass(scene, camera); + composer.addPass(renderPass); + saoPass = new SAOPass(scene, camera); + composer.addPass(saoPass); + const outputPass = new OutputPass(); + composer.addPass(outputPass); + + // Init gui + const gui = new GUI(); + gui.add(saoPass.params, 'output', { + Default: SAOPass.OUTPUT.Default, + 'SAO Only': SAOPass.OUTPUT.SAO, + Normal: SAOPass.OUTPUT.Normal, + }).onChange(function (value) { + saoPass.params.output = value; + }); + gui.add(saoPass.params, 'saoBias', -1, 1); + gui.add(saoPass.params, 'saoIntensity', 0, 1); + gui.add(saoPass.params, 'saoScale', 0, 10); + gui.add(saoPass.params, 'saoKernelRadius', 1, 100); + gui.add(saoPass.params, 'saoMinResolution', 0, 1); + gui.add(saoPass.params, 'saoBlur'); + gui.add(saoPass.params, 'saoBlurRadius', 0, 200); + gui.add(saoPass.params, 'saoBlurStdDev', 0.5, 150); + gui.add(saoPass.params, 'saoBlurDepthCutoff', 0.0, 0.1); + gui.add(saoPass, 'enabled'); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const width = window.innerWidth || 1; + const height = window.innerHeight || 1; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + renderer.setSize(width, height); + + composer.setSize(width, height); +} + +function animate() { + stats.begin(); + render(); + stats.end(); +} + +function render() { + const timer = performance.now(); + group.rotation.x = timer * 0.0002; + group.rotation.y = timer * 0.0001; + + composer.render(); +} diff --git a/examples-testing/examples/webgl_postprocessing_smaa.ts b/examples-testing/examples/webgl_postprocessing_smaa.ts new file mode 100644 index 000000000..6f71f6478 --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_smaa.ts @@ -0,0 +1,109 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +let camera, scene, renderer, composer, stats, smaaPass; + +const params = { + enabled: true, + autoRotate: true, +}; + +init(); + +function init() { + const container = document.getElementById('container'); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 300; + + scene = new THREE.Scene(); + + const geometry = new THREE.BoxGeometry(120, 120, 120); + const material1 = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true }); + + const mesh1 = new THREE.Mesh(geometry, material1); + mesh1.position.x = -100; + scene.add(mesh1); + + const texture = new THREE.TextureLoader().load('textures/brick_diffuse.jpg'); + texture.anisotropy = renderer.capabilities.getMaxAnisotropy(); + texture.colorSpace = THREE.SRGBColorSpace; + + const material2 = new THREE.MeshBasicMaterial({ map: texture }); + + const mesh2 = new THREE.Mesh(geometry, material2); + mesh2.position.x = 100; + scene.add(mesh2); + + // postprocessing + + composer = new EffectComposer(renderer); + composer.addPass(new RenderPass(scene, camera)); + + smaaPass = new SMAAPass( + window.innerWidth * renderer.getPixelRatio(), + window.innerHeight * renderer.getPixelRatio(), + ); + composer.addPass(smaaPass); + + const outputPass = new OutputPass(); + composer.addPass(outputPass); + + window.addEventListener('resize', onWindowResize); + + const gui = new GUI(); + + const smaaFolder = gui.addFolder('SMAA'); + smaaFolder.add(params, 'enabled'); + + const sceneFolder = gui.addFolder('Scene'); + sceneFolder.add(params, 'autoRotate'); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + composer.setSize(width, height); +} + +function animate() { + stats.begin(); + + if (params.autoRotate === true) { + for (let i = 0; i < scene.children.length; i++) { + const child = scene.children[i]; + + child.rotation.x += 0.005; + child.rotation.y += 0.01; + } + } + + smaaPass.enabled = params.enabled; + + composer.render(); + + stats.end(); +} diff --git a/examples-testing/examples/webgl_postprocessing_sobel.ts b/examples-testing/examples/webgl_postprocessing_sobel.ts new file mode 100644 index 000000000..55d88dc02 --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_sobel.ts @@ -0,0 +1,111 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; + +import { LuminosityShader } from 'three/addons/shaders/LuminosityShader.js'; +import { SobelOperatorShader } from 'three/addons/shaders/SobelOperatorShader.js'; + +let camera, scene, renderer, composer; + +let effectSobel; + +const params = { + enable: true, +}; + +init(); + +function init() { + // + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0, 1, 3); + camera.lookAt(scene.position); + + // + + const geometry = new THREE.TorusKnotGeometry(1, 0.3, 256, 32); + const material = new THREE.MeshPhongMaterial({ color: 0xffff00 }); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + const ambientLight = new THREE.AmbientLight(0xe7e7e7); + scene.add(ambientLight); + + const pointLight = new THREE.PointLight(0xffffff, 20); + camera.add(pointLight); + scene.add(camera); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // postprocessing + + composer = new EffectComposer(renderer); + const renderPass = new RenderPass(scene, camera); + composer.addPass(renderPass); + + // color to grayscale conversion + + const effectGrayScale = new ShaderPass(LuminosityShader); + composer.addPass(effectGrayScale); + + // you might want to use a gaussian blur filter before + // the next pass to improve the result of the Sobel operator + + // Sobel operator + + effectSobel = new ShaderPass(SobelOperatorShader); + effectSobel.uniforms['resolution'].value.x = window.innerWidth * window.devicePixelRatio; + effectSobel.uniforms['resolution'].value.y = window.innerHeight * window.devicePixelRatio; + composer.addPass(effectSobel); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableZoom = false; + + // + + const gui = new GUI(); + + gui.add(params, 'enable'); + gui.open(); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); + + effectSobel.uniforms['resolution'].value.x = window.innerWidth * window.devicePixelRatio; + effectSobel.uniforms['resolution'].value.y = window.innerHeight * window.devicePixelRatio; +} + +function animate() { + if (params.enable === true) { + composer.render(); + } else { + renderer.render(scene, camera); + } +} diff --git a/examples-testing/examples/webgl_postprocessing_ssaa.ts b/examples-testing/examples/webgl_postprocessing_ssaa.ts new file mode 100644 index 000000000..429e02dee --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_ssaa.ts @@ -0,0 +1,206 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { SSAARenderPass } from 'three/addons/postprocessing/SSAARenderPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +let scene, renderer, composer; +let cameraP, ssaaRenderPassP; +let cameraO, ssaaRenderPassO; +let gui, stats; + +const params = { + sampleLevel: 4, + unbiased: true, + camera: 'perspective', + clearColor: 'black', + clearAlpha: 1.0, + viewOffsetX: 0, + autoRotate: true, +}; + +init(); + +clearGui(); + +function clearGui() { + if (gui) gui.destroy(); + + gui = new GUI(); + + gui.add(params, 'unbiased'); + gui.add(params, 'sampleLevel', { + 'Level 0: 1 Sample': 0, + 'Level 1: 2 Samples': 1, + 'Level 2: 4 Samples': 2, + 'Level 3: 8 Samples': 3, + 'Level 4: 16 Samples': 4, + 'Level 5: 32 Samples': 5, + }); + gui.add(params, 'camera', ['perspective', 'orthographic']); + gui.add(params, 'clearColor', ['black', 'white', 'blue', 'green', 'red']); + gui.add(params, 'clearAlpha', 0, 1); + gui.add(params, 'viewOffsetX', -100, 100); + gui.add(params, 'autoRotate'); + + gui.open(); +} + +function init() { + const container = document.getElementById('container'); + + const width = window.innerWidth || 1; + const height = window.innerHeight || 1; + const aspect = width / height; + const devicePixelRatio = window.devicePixelRatio || 1; + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(devicePixelRatio); + renderer.setSize(width, height); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + cameraP = new THREE.PerspectiveCamera(65, aspect, 3, 10); + cameraP.position.z = 7; + cameraP.setViewOffset(width, height, params.viewOffsetX, 0, width, height); + + cameraO = new THREE.OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 3, 10); + cameraO.position.z = 7; + + const fov = THREE.MathUtils.degToRad(cameraP.fov); + const hyperfocus = (cameraP.near + cameraP.far) / 2; + const _height = 2 * Math.tan(fov / 2) * hyperfocus; + cameraO.zoom = height / _height; + + scene = new THREE.Scene(); + + const group = new THREE.Group(); + scene.add(group); + + const light = new THREE.PointLight(0xefffef, 500); + light.position.z = 10; + light.position.y = -10; + light.position.x = -10; + scene.add(light); + + const light2 = new THREE.PointLight(0xffefef, 500); + light2.position.z = 10; + light2.position.x = -10; + light2.position.y = 10; + scene.add(light2); + + const light3 = new THREE.PointLight(0xefefff, 500); + light3.position.z = 10; + light3.position.x = 10; + light3.position.y = -10; + scene.add(light3); + + const light4 = new THREE.AmbientLight(0xffffff, 0.2); + scene.add(light4); + + const geometry = new THREE.SphereGeometry(3, 48, 24); + + for (let i = 0; i < 120; i++) { + const material = new THREE.MeshStandardMaterial(); + material.roughness = 0.5 * Math.random() + 0.25; + material.metalness = 0; + material.color.setHSL(Math.random(), 1.0, 0.3); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.x = Math.random() * 4 - 2; + mesh.position.y = Math.random() * 4 - 2; + mesh.position.z = Math.random() * 4 - 2; + mesh.rotation.x = Math.random(); + mesh.rotation.y = Math.random(); + mesh.rotation.z = Math.random(); + + mesh.scale.setScalar(Math.random() * 0.2 + 0.05); + group.add(mesh); + } + + // postprocessing + + composer = new EffectComposer(renderer); + composer.setPixelRatio(1); // ensure pixel ratio is always 1 for performance reasons + ssaaRenderPassP = new SSAARenderPass(scene, cameraP); + composer.addPass(ssaaRenderPassP); + ssaaRenderPassO = new SSAARenderPass(scene, cameraO); + composer.addPass(ssaaRenderPassO); + const outputPass = new OutputPass(); + composer.addPass(outputPass); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + const aspect = width / height; + + cameraP.aspect = aspect; + cameraP.setViewOffset(width, height, params.viewOffsetX, 0, width, height); + cameraO.updateProjectionMatrix(); + + cameraO.left = -height * aspect; + cameraO.right = height * aspect; + cameraO.top = height; + cameraO.bottom = -height; + cameraO.updateProjectionMatrix(); + + renderer.setSize(width, height); + composer.setSize(width, height); +} + +function animate() { + stats.begin(); + + if (params.autoRotate) { + for (let i = 0; i < scene.children.length; i++) { + const child = scene.children[i]; + + child.rotation.x += 0.005; + child.rotation.y += 0.01; + } + } + + let newColor = ssaaRenderPassP.clearColor; + + switch (params.clearColor) { + case 'blue': + newColor = 0x0000ff; + break; + case 'red': + newColor = 0xff0000; + break; + case 'green': + newColor = 0x00ff00; + break; + case 'white': + newColor = 0xffffff; + break; + case 'black': + newColor = 0x000000; + break; + } + + ssaaRenderPassP.clearColor = ssaaRenderPassO.clearColor = newColor; + ssaaRenderPassP.clearAlpha = ssaaRenderPassO.clearAlpha = params.clearAlpha; + + ssaaRenderPassP.sampleLevel = ssaaRenderPassO.sampleLevel = params.sampleLevel; + ssaaRenderPassP.unbiased = ssaaRenderPassO.unbiased = params.unbiased; + + ssaaRenderPassP.enabled = params.camera === 'perspective'; + ssaaRenderPassO.enabled = params.camera === 'orthographic'; + + cameraP.view.offsetX = params.viewOffsetX; + + composer.render(); + + stats.end(); +} diff --git a/examples-testing/examples/webgl_postprocessing_ssao.ts b/examples-testing/examples/webgl_postprocessing_ssao.ts new file mode 100644 index 000000000..e55ab0446 --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_ssao.ts @@ -0,0 +1,118 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { SSAOPass } from 'three/addons/postprocessing/SSAOPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +let container, stats; +let camera, scene, renderer; +let composer; +let group; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + renderer = new THREE.WebGLRenderer(); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 100, 700); + camera.position.z = 500; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xaaaaaa); + + scene.add(new THREE.DirectionalLight(0xffffff, 4)); + scene.add(new THREE.AmbientLight(0xffffff)); + + group = new THREE.Group(); + scene.add(group); + + const geometry = new THREE.BoxGeometry(10, 10, 10); + + for (let i = 0; i < 100; i++) { + const material = new THREE.MeshLambertMaterial({ + color: Math.random() * 0xffffff, + }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.x = Math.random() * 400 - 200; + mesh.position.y = Math.random() * 400 - 200; + mesh.position.z = Math.random() * 400 - 200; + mesh.rotation.x = Math.random(); + mesh.rotation.y = Math.random(); + mesh.rotation.z = Math.random(); + + mesh.scale.setScalar(Math.random() * 10 + 2); + group.add(mesh); + } + + stats = new Stats(); + container.appendChild(stats.dom); + + const width = window.innerWidth; + const height = window.innerHeight; + + composer = new EffectComposer(renderer); + + const renderPass = new RenderPass(scene, camera); + composer.addPass(renderPass); + + const ssaoPass = new SSAOPass(scene, camera, width, height); + composer.addPass(ssaoPass); + + const outputPass = new OutputPass(); + composer.addPass(outputPass); + + // Init gui + const gui = new GUI(); + + gui.add(ssaoPass, 'output', { + Default: SSAOPass.OUTPUT.Default, + 'SSAO Only': SSAOPass.OUTPUT.SSAO, + 'SSAO Only + Blur': SSAOPass.OUTPUT.Blur, + Depth: SSAOPass.OUTPUT.Depth, + Normal: SSAOPass.OUTPUT.Normal, + }).onChange(function (value) { + ssaoPass.output = value; + }); + gui.add(ssaoPass, 'kernelRadius').min(0).max(32); + gui.add(ssaoPass, 'minDistance').min(0.001).max(0.02); + gui.add(ssaoPass, 'maxDistance').min(0.01).max(0.3); + gui.add(ssaoPass, 'enabled'); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + composer.setSize(width, height); +} + +function animate() { + stats.begin(); + render(); + stats.end(); +} + +function render() { + const timer = performance.now(); + group.rotation.x = timer * 0.0002; + group.rotation.y = timer * 0.0001; + + composer.render(); +} diff --git a/examples-testing/examples/webgl_postprocessing_ssr.ts b/examples-testing/examples/webgl_postprocessing_ssr.ts new file mode 100644 index 000000000..307cfd1de --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_ssr.ts @@ -0,0 +1,261 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { SSRPass } from 'three/addons/postprocessing/SSRPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; +import { ReflectorForSSRPass } from 'three/addons/objects/ReflectorForSSRPass.js'; + +import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; + +const params = { + enableSSR: true, + autoRotate: true, + otherMeshes: true, + groundReflector: true, +}; +let composer; +let ssrPass; +let gui; +let stats; +let controls; +let camera, scene, renderer; +const otherMeshes = []; +let groundReflector; +const selects = []; + +const container = document.querySelector('#container'); + +// Configure and create Draco decoder. +const dracoLoader = new DRACOLoader(); +dracoLoader.setDecoderPath('jsm/libs/draco/'); +dracoLoader.setDecoderConfig({ type: 'js' }); + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 15); + camera.position.set(0.13271600513224902, 0.3489546826045913, 0.43921296427927076); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x443333); + scene.fog = new THREE.Fog(0x443333, 1, 4); + + // Ground + const plane = new THREE.Mesh(new THREE.PlaneGeometry(8, 8), new THREE.MeshPhongMaterial({ color: 0xcbcbcb })); + plane.rotation.x = -Math.PI / 2; + plane.position.y = -0.0001; + // plane.receiveShadow = true; + scene.add(plane); + + // Lights + const hemiLight = new THREE.HemisphereLight(0x8d7c7c, 0x494966, 3); + scene.add(hemiLight); + + const spotLight = new THREE.SpotLight(); + spotLight.intensity = 8; + spotLight.angle = Math.PI / 16; + spotLight.penumbra = 0.5; + // spotLight.castShadow = true; + spotLight.position.set(-1, 1, 1); + scene.add(spotLight); + + dracoLoader.load('models/draco/bunny.drc', function (geometry) { + geometry.computeVertexNormals(); + + const material = new THREE.MeshStandardMaterial({ color: 0xa5a5a5 }); + const mesh = new THREE.Mesh(geometry, material); + mesh.position.y = -0.0365; + scene.add(mesh); + selects.push(mesh); + + // Release decoder resources. + dracoLoader.dispose(); + }); + + let geometry, material, mesh; + + geometry = new THREE.BoxGeometry(0.05, 0.05, 0.05); + material = new THREE.MeshStandardMaterial({ color: 'green' }); + mesh = new THREE.Mesh(geometry, material); + mesh.position.set(-0.12, 0.025, 0.015); + scene.add(mesh); + otherMeshes.push(mesh); + selects.push(mesh); + + geometry = new THREE.IcosahedronGeometry(0.025, 4); + material = new THREE.MeshStandardMaterial({ color: 'cyan' }); + mesh = new THREE.Mesh(geometry, material); + mesh.position.set(-0.05, 0.025, 0.08); + scene.add(mesh); + otherMeshes.push(mesh); + selects.push(mesh); + + geometry = new THREE.ConeGeometry(0.025, 0.05, 64); + material = new THREE.MeshStandardMaterial({ color: 'yellow' }); + mesh = new THREE.Mesh(geometry, material); + mesh.position.set(-0.05, 0.025, -0.055); + scene.add(mesh); + otherMeshes.push(mesh); + selects.push(mesh); + + geometry = new THREE.PlaneGeometry(1, 1); + groundReflector = new ReflectorForSSRPass(geometry, { + clipBias: 0.0003, + textureWidth: window.innerWidth, + textureHeight: window.innerHeight, + color: 0x888888, + useDepthTexture: true, + }); + groundReflector.material.depthWrite = false; + groundReflector.rotation.x = -Math.PI / 2; + groundReflector.visible = false; + scene.add(groundReflector); + + // renderer + renderer = new THREE.WebGLRenderer({ antialias: false }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.target.set(0, 0.0635, 0); + controls.update(); + controls.enabled = !params.autoRotate; + + // STATS + + stats = new Stats(); + container.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); + + // composer + + composer = new EffectComposer(renderer); + ssrPass = new SSRPass({ + renderer, + scene, + camera, + width: innerWidth, + height: innerHeight, + groundReflector: params.groundReflector ? groundReflector : null, + selects: params.groundReflector ? selects : null, + }); + + composer.addPass(ssrPass); + composer.addPass(new OutputPass()); + + // GUI + + gui = new GUI({ width: 260 }); + gui.add(params, 'enableSSR').name('Enable SSR'); + gui.add(params, 'groundReflector').onChange(() => { + if (params.groundReflector) { + (ssrPass.groundReflector = groundReflector), (ssrPass.selects = selects); + } else { + (ssrPass.groundReflector = null), (ssrPass.selects = null); + } + }); + ssrPass.thickness = 0.018; + gui.add(ssrPass, 'thickness').min(0).max(0.1).step(0.0001); + ssrPass.infiniteThick = false; + gui.add(ssrPass, 'infiniteThick'); + gui.add(params, 'autoRotate').onChange(() => { + controls.enabled = !params.autoRotate; + }); + + const folder = gui.addFolder('more settings'); + folder.add(ssrPass, 'fresnel').onChange(() => { + groundReflector.fresnel = ssrPass.fresnel; + }); + folder.add(ssrPass, 'distanceAttenuation').onChange(() => { + groundReflector.distanceAttenuation = ssrPass.distanceAttenuation; + }); + ssrPass.maxDistance = 0.1; + groundReflector.maxDistance = ssrPass.maxDistance; + folder + .add(ssrPass, 'maxDistance') + .min(0) + .max(0.5) + .step(0.001) + .onChange(() => { + groundReflector.maxDistance = ssrPass.maxDistance; + }); + folder.add(params, 'otherMeshes').onChange(() => { + if (params.otherMeshes) { + otherMeshes.forEach(mesh => (mesh.visible = true)); + } else { + otherMeshes.forEach(mesh => (mesh.visible = false)); + } + }); + folder.add(ssrPass, 'bouncing'); + folder + .add(ssrPass, 'output', { + Default: SSRPass.OUTPUT.Default, + 'SSR Only': SSRPass.OUTPUT.SSR, + Beauty: SSRPass.OUTPUT.Beauty, + Depth: SSRPass.OUTPUT.Depth, + Normal: SSRPass.OUTPUT.Normal, + Metalness: SSRPass.OUTPUT.Metalness, + }) + .onChange(function (value) { + ssrPass.output = value; + }); + ssrPass.opacity = 1; + groundReflector.opacity = ssrPass.opacity; + folder + .add(ssrPass, 'opacity') + .min(0) + .max(1) + .onChange(() => { + groundReflector.opacity = ssrPass.opacity; + }); + folder.add(ssrPass, 'blur'); + // folder.open() + // gui.close() +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); + groundReflector.getRenderTarget().setSize(window.innerWidth, window.innerHeight); + groundReflector.resolution.set(window.innerWidth, window.innerHeight); +} + +function animate() { + stats.begin(); + render(); + stats.end(); +} + +function render() { + if (params.autoRotate) { + const timer = Date.now() * 0.0003; + + camera.position.x = Math.sin(timer) * 0.5; + camera.position.y = 0.2135; + camera.position.z = Math.cos(timer) * 0.5; + camera.lookAt(0, 0.0635, 0); + } else { + controls.update(); + } + + if (params.enableSSR) { + // TODO: groundReflector has full ground info, need use it to solve reflection gaps problem on objects when camera near ground. + // TODO: the normal and depth info where groundReflector reflected need to be changed. + composer.render(); + } else { + renderer.render(scene, camera); + } +} diff --git a/examples-testing/examples/webgl_postprocessing_taa.ts b/examples-testing/examples/webgl_postprocessing_taa.ts new file mode 100644 index 000000000..11a986741 --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_taa.ts @@ -0,0 +1,139 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { TAARenderPass } from 'three/addons/postprocessing/TAARenderPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +let camera, scene, renderer, composer, taaRenderPass, renderPass; +let gui, stats; +let index = 0; + +const param = { TAAEnabled: '1', TAASampleLevel: 0 }; + +init(); + +clearGui(); + +function clearGui() { + if (gui) gui.destroy(); + + gui = new GUI(); + + gui.add(param, 'TAAEnabled', { + Disabled: '0', + Enabled: '1', + }).onFinishChange(function () { + if (taaRenderPass) { + taaRenderPass.enabled = param.TAAEnabled === '1'; + renderPass.enabled = param.TAAEnabled !== '1'; + } + }); + + gui.add(param, 'TAASampleLevel', { + 'Level 0: 1 Sample': 0, + 'Level 1: 2 Samples': 1, + 'Level 2: 4 Samples': 2, + 'Level 3: 8 Samples': 3, + 'Level 4: 16 Samples': 4, + 'Level 5: 32 Samples': 5, + }).onFinishChange(function () { + if (taaRenderPass) { + taaRenderPass.sampleLevel = param.TAASampleLevel; + } + }); + + gui.open(); +} + +function init() { + const container = document.getElementById('container'); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 300; + + scene = new THREE.Scene(); + + const geometry = new THREE.BoxGeometry(120, 120, 120); + const material1 = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true }); + + const mesh1 = new THREE.Mesh(geometry, material1); + mesh1.position.x = -100; + scene.add(mesh1); + + const texture = new THREE.TextureLoader().load('textures/brick_diffuse.jpg'); + texture.minFilter = THREE.NearestFilter; + texture.magFilter = THREE.NearestFilter; + texture.anisotropy = 1; + texture.generateMipmaps = false; + texture.colorSpace = THREE.SRGBColorSpace; + + const material2 = new THREE.MeshBasicMaterial({ map: texture }); + + const mesh2 = new THREE.Mesh(geometry, material2); + mesh2.position.x = 100; + scene.add(mesh2); + + // postprocessing + + composer = new EffectComposer(renderer); + + taaRenderPass = new TAARenderPass(scene, camera); + taaRenderPass.unbiased = false; + composer.addPass(taaRenderPass); + + renderPass = new RenderPass(scene, camera); + renderPass.enabled = false; + composer.addPass(renderPass); + + const outputPass = new OutputPass(); + composer.addPass(outputPass); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + composer.setSize(width, height); +} + +function animate() { + index++; + + if (Math.round(index / 200) % 2 === 0) { + for (let i = 0; i < scene.children.length; i++) { + const child = scene.children[i]; + + child.rotation.x += 0.005; + child.rotation.y += 0.01; + } + + if (taaRenderPass) taaRenderPass.accumulate = false; + } else { + if (taaRenderPass) taaRenderPass.accumulate = true; + } + + composer.render(); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_postprocessing_transition.ts b/examples-testing/examples/webgl_postprocessing_transition.ts new file mode 100644 index 000000000..d05466131 --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_transition.ts @@ -0,0 +1,211 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import TWEEN from 'three/addons/libs/tween.module.js'; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderTransitionPass } from 'three/addons/postprocessing/RenderTransitionPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +let stats; +let renderer, composer, renderTransitionPass; + +const textures = []; +const clock = new THREE.Clock(); + +const params = { + sceneAnimate: true, + transitionAnimate: true, + transition: 0, + useTexture: true, + texture: 5, + cycle: true, + threshold: 0.1, +}; + +const fxSceneA = new FXScene(new THREE.BoxGeometry(2, 2, 2), new THREE.Vector3(0, -0.4, 0), 0xffffff); +const fxSceneB = new FXScene(new THREE.IcosahedronGeometry(1, 1), new THREE.Vector3(0, 0.2, 0.1), 0x000000); + +init(); + +function init() { + initGUI(); + initTextures(); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + composer = new EffectComposer(renderer); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + renderTransitionPass = new RenderTransitionPass(fxSceneA.scene, fxSceneA.camera, fxSceneB.scene, fxSceneB.camera); + renderTransitionPass.setTexture(textures[0]); + composer.addPass(renderTransitionPass); + + const outputPass = new OutputPass(); + composer.addPass(outputPass); +} + +window.addEventListener('resize', onWindowResize); + +function onWindowResize() { + fxSceneA.resize(); + fxSceneB.resize(); + renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); +} + +new TWEEN.Tween(params) + .to({ transition: 1 }, 1500) + .onUpdate(function () { + renderTransitionPass.setTransition(params.transition); + + // Change the current alpha texture after each transition + if (params.cycle) { + if (params.transition == 0 || params.transition == 1) { + params.texture = (params.texture + 1) % textures.length; + renderTransitionPass.setTexture(textures[params.texture]); + } + } + }) + .repeat(Infinity) + .delay(2000) + .yoyo(true) + .start(); + +function animate() { + // Transition animation + if (params.transitionAnimate) TWEEN.update(); + + const delta = clock.getDelta(); + fxSceneA.update(delta); + fxSceneB.update(delta); + + render(); + stats.update(); +} + +function initTextures() { + const loader = new THREE.TextureLoader(); + + for (let i = 0; i < 6; i++) { + textures[i] = loader.load('textures/transition/transition' + (i + 1) + '.png'); + } +} + +function initGUI() { + const gui = new GUI(); + + gui.add(params, 'sceneAnimate').name('Animate scene'); + gui.add(params, 'transitionAnimate').name('Animate transition'); + gui.add(params, 'transition', 0, 1, 0.01) + .onChange(function (value) { + renderTransitionPass.setTransition(value); + }) + .listen(); + + gui.add(params, 'useTexture').onChange(function (value) { + renderTransitionPass.useTexture(value); + }); + + gui.add(params, 'texture', { Perlin: 0, Squares: 1, Cells: 2, Distort: 3, Gradient: 4, Radial: 5 }) + .onChange(function (value) { + renderTransitionPass.setTexture(textures[value]); + }) + .listen(); + + gui.add(params, 'cycle'); + + gui.add(params, 'threshold', 0, 1, 0.01).onChange(function (value) { + renderTransitionPass.setTextureThreshold(value); + }); +} + +function render() { + // Prevent render both scenes when it's not necessary + if (params.transition === 0) { + renderer.render(fxSceneB.scene, fxSceneB.camera); + } else if (params.transition === 1) { + renderer.render(fxSceneA.scene, fxSceneA.camera); + } else { + // When 0 < transition < 1 render transition between two scenes + composer.render(); + } +} + +function FXScene(geometry, rotationSpeed, backgroundColor) { + const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.z = 20; + + // Setup scene + const scene = new THREE.Scene(); + scene.background = new THREE.Color(backgroundColor); + scene.add(new THREE.AmbientLight(0xaaaaaa, 3)); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0, 1, 4); + scene.add(light); + + this.rotationSpeed = rotationSpeed; + + const color = geometry.type === 'BoxGeometry' ? 0x0000ff : 0xff0000; + const material = new THREE.MeshPhongMaterial({ color: color, flatShading: true }); + const mesh = generateInstancedMesh(geometry, material, 500); + scene.add(mesh); + + this.scene = scene; + this.camera = camera; + this.mesh = mesh; + + this.update = function (delta) { + if (params.sceneAnimate) { + mesh.rotation.x += this.rotationSpeed.x * delta; + mesh.rotation.y += this.rotationSpeed.y * delta; + mesh.rotation.z += this.rotationSpeed.z * delta; + } + }; + + this.resize = function () { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + }; +} + +function generateInstancedMesh(geometry, material, count) { + const mesh = new THREE.InstancedMesh(geometry, material, count); + + const dummy = new THREE.Object3D(); + const color = new THREE.Color(); + + for (let i = 0; i < count; i++) { + dummy.position.x = Math.random() * 100 - 50; + dummy.position.y = Math.random() * 60 - 30; + dummy.position.z = Math.random() * 80 - 40; + + dummy.rotation.x = Math.random() * 2 * Math.PI; + dummy.rotation.y = Math.random() * 2 * Math.PI; + dummy.rotation.z = Math.random() * 2 * Math.PI; + + dummy.scale.x = Math.random() * 2 + 1; + + if (geometry.type === 'BoxGeometry') { + dummy.scale.y = Math.random() * 2 + 1; + dummy.scale.z = Math.random() * 2 + 1; + } else { + dummy.scale.y = dummy.scale.x; + dummy.scale.z = dummy.scale.x; + } + + dummy.updateMatrix(); + + mesh.setMatrixAt(i, dummy.matrix); + mesh.setColorAt(i, color.setScalar(0.1 + 0.9 * Math.random())); + } + + return mesh; +} diff --git a/examples-testing/examples/webgl_postprocessing_unreal_bloom.ts b/examples-testing/examples/webgl_postprocessing_unreal_bloom.ts new file mode 100644 index 000000000..53ec2fe2f --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_unreal_bloom.ts @@ -0,0 +1,136 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +let camera, stats; +let composer, renderer, mixer, clock; + +const params = { + threshold: 0, + strength: 1, + radius: 0, + exposure: 1, +}; + +init(); + +async function init() { + const container = document.getElementById('container'); + + clock = new THREE.Clock(); + + const scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 100); + camera.position.set(-5, 2.5, -3.5); + scene.add(camera); + + scene.add(new THREE.AmbientLight(0xcccccc)); + + const pointLight = new THREE.PointLight(0xffffff, 100); + camera.add(pointLight); + + const loader = new GLTFLoader(); + const gltf = await loader.loadAsync('models/gltf/PrimaryIonDrive.glb'); + + const model = gltf.scene; + scene.add(model); + + mixer = new THREE.AnimationMixer(model); + const clip = gltf.animations[0]; + mixer.clipAction(clip.optimize()).play(); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ReinhardToneMapping; + container.appendChild(renderer.domElement); + + // + + const renderScene = new RenderPass(scene, camera); + + const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85); + bloomPass.threshold = params.threshold; + bloomPass.strength = params.strength; + bloomPass.radius = params.radius; + + const outputPass = new OutputPass(); + + composer = new EffectComposer(renderer); + composer.addPass(renderScene); + composer.addPass(bloomPass); + composer.addPass(outputPass); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.maxPolarAngle = Math.PI * 0.5; + controls.minDistance = 3; + controls.maxDistance = 8; + + // + + const gui = new GUI(); + + const bloomFolder = gui.addFolder('bloom'); + + bloomFolder.add(params, 'threshold', 0.0, 1.0).onChange(function (value) { + bloomPass.threshold = Number(value); + }); + + bloomFolder.add(params, 'strength', 0.0, 3.0).onChange(function (value) { + bloomPass.strength = Number(value); + }); + + gui.add(params, 'radius', 0.0, 1.0) + .step(0.01) + .onChange(function (value) { + bloomPass.radius = Number(value); + }); + + const toneMappingFolder = gui.addFolder('tone mapping'); + + toneMappingFolder.add(params, 'exposure', 0.1, 2).onChange(function (value) { + renderer.toneMappingExposure = Math.pow(value, 4.0); + }); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + composer.setSize(width, height); +} + +function animate() { + const delta = clock.getDelta(); + + mixer.update(delta); + + stats.update(); + + composer.render(); +} diff --git a/examples-testing/examples/webgl_postprocessing_unreal_bloom_selective.ts b/examples-testing/examples/webgl_postprocessing_unreal_bloom_selective.ts new file mode 100644 index 000000000..d633806ee --- /dev/null +++ b/examples-testing/examples/webgl_postprocessing_unreal_bloom_selective.ts @@ -0,0 +1,195 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; +import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +const BLOOM_SCENE = 1; + +const bloomLayer = new THREE.Layers(); +bloomLayer.set(BLOOM_SCENE); + +const params = { + threshold: 0, + strength: 1, + radius: 0.5, + exposure: 1, +}; + +const darkMaterial = new THREE.MeshBasicMaterial({ color: 'black' }); +const materials = {}; + +const renderer = new THREE.WebGLRenderer({ antialias: true }); +renderer.setPixelRatio(window.devicePixelRatio); +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.toneMapping = THREE.ReinhardToneMapping; +document.body.appendChild(renderer.domElement); + +const scene = new THREE.Scene(); + +const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 200); +camera.position.set(0, 0, 20); +camera.lookAt(0, 0, 0); + +const controls = new OrbitControls(camera, renderer.domElement); +controls.maxPolarAngle = Math.PI * 0.5; +controls.minDistance = 1; +controls.maxDistance = 100; +controls.addEventListener('change', render); + +const renderScene = new RenderPass(scene, camera); + +const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85); +bloomPass.threshold = params.threshold; +bloomPass.strength = params.strength; +bloomPass.radius = params.radius; + +const bloomComposer = new EffectComposer(renderer); +bloomComposer.renderToScreen = false; +bloomComposer.addPass(renderScene); +bloomComposer.addPass(bloomPass); + +const mixPass = new ShaderPass( + new THREE.ShaderMaterial({ + uniforms: { + baseTexture: { value: null }, + bloomTexture: { value: bloomComposer.renderTarget2.texture }, + }, + vertexShader: document.getElementById('vertexshader').textContent, + fragmentShader: document.getElementById('fragmentshader').textContent, + defines: {}, + }), + 'baseTexture', +); +mixPass.needsSwap = true; + +const outputPass = new OutputPass(); + +const finalComposer = new EffectComposer(renderer); +finalComposer.addPass(renderScene); +finalComposer.addPass(mixPass); +finalComposer.addPass(outputPass); + +const raycaster = new THREE.Raycaster(); + +const mouse = new THREE.Vector2(); + +window.addEventListener('pointerdown', onPointerDown); + +const gui = new GUI(); + +const bloomFolder = gui.addFolder('bloom'); + +bloomFolder.add(params, 'threshold', 0.0, 1.0).onChange(function (value) { + bloomPass.threshold = Number(value); + render(); +}); + +bloomFolder.add(params, 'strength', 0.0, 3).onChange(function (value) { + bloomPass.strength = Number(value); + render(); +}); + +bloomFolder + .add(params, 'radius', 0.0, 1.0) + .step(0.01) + .onChange(function (value) { + bloomPass.radius = Number(value); + render(); + }); + +const toneMappingFolder = gui.addFolder('tone mapping'); + +toneMappingFolder.add(params, 'exposure', 0.1, 2).onChange(function (value) { + renderer.toneMappingExposure = Math.pow(value, 4.0); + render(); +}); + +setupScene(); + +function onPointerDown(event) { + mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; + + raycaster.setFromCamera(mouse, camera); + const intersects = raycaster.intersectObjects(scene.children, false); + if (intersects.length > 0) { + const object = intersects[0].object; + object.layers.toggle(BLOOM_SCENE); + render(); + } +} + +window.onresize = function () { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + + bloomComposer.setSize(width, height); + finalComposer.setSize(width, height); + + render(); +}; + +function setupScene() { + scene.traverse(disposeMaterial); + scene.children.length = 0; + + const geometry = new THREE.IcosahedronGeometry(1, 15); + + for (let i = 0; i < 50; i++) { + const color = new THREE.Color(); + color.setHSL(Math.random(), 0.7, Math.random() * 0.2 + 0.05); + + const material = new THREE.MeshBasicMaterial({ color: color }); + const sphere = new THREE.Mesh(geometry, material); + sphere.position.x = Math.random() * 10 - 5; + sphere.position.y = Math.random() * 10 - 5; + sphere.position.z = Math.random() * 10 - 5; + sphere.position.normalize().multiplyScalar(Math.random() * 4.0 + 2.0); + sphere.scale.setScalar(Math.random() * Math.random() + 0.5); + scene.add(sphere); + + if (Math.random() < 0.25) sphere.layers.enable(BLOOM_SCENE); + } + + render(); +} + +function disposeMaterial(obj) { + if (obj.material) { + obj.material.dispose(); + } +} + +function render() { + scene.traverse(darkenNonBloomed); + bloomComposer.render(); + scene.traverse(restoreMaterial); + + // render the entire scene, then render bloom scene on top + finalComposer.render(); +} + +function darkenNonBloomed(obj) { + if (obj.isMesh && bloomLayer.test(obj.layers) === false) { + materials[obj.uuid] = obj.material; + obj.material = darkMaterial; + } +} + +function restoreMaterial(obj) { + if (materials[obj.uuid]) { + obj.material = materials[obj.uuid]; + delete materials[obj.uuid]; + } +} diff --git a/examples-testing/examples/webgl_raycaster_sprite.ts b/examples-testing/examples/webgl_raycaster_sprite.ts new file mode 100644 index 000000000..f35d5de17 --- /dev/null +++ b/examples-testing/examples/webgl_raycaster_sprite.ts @@ -0,0 +1,103 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let renderer, scene, camera; +let group; + +let selectedObject = null; +const raycaster = new THREE.Raycaster(); +const pointer = new THREE.Vector2(); + +init(); + +function init() { + // init renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // init scene + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + + group = new THREE.Group(); + scene.add(group); + + // init camera + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(15, 15, 15); + camera.lookAt(scene.position); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 15; + controls.maxDistance = 250; + + // add sprites + + const sprite1 = new THREE.Sprite(new THREE.SpriteMaterial({ color: '#69f' })); + sprite1.position.set(6, 5, 5); + sprite1.scale.set(2, 5, 1); + group.add(sprite1); + + const sprite2 = new THREE.Sprite(new THREE.SpriteMaterial({ color: '#69f', sizeAttenuation: false })); + sprite2.material.rotation = (Math.PI / 3) * 4; + sprite2.position.set(8, -2, 2); + sprite2.center.set(0.5, 0); + sprite2.scale.set(0.1, 0.5, 0.1); + group.add(sprite2); + + const group2 = new THREE.Object3D(); + group2.scale.set(1, 2, 1); + group2.position.set(-5, 0, 0); + group2.rotation.set(Math.PI / 2, 0, 0); + group.add(group2); + + const sprite3 = new THREE.Sprite(new THREE.SpriteMaterial({ color: '#69f' })); + sprite3.position.set(0, 2, 5); + sprite3.scale.set(10, 2, 3); + sprite3.center.set(-0.1, 0); + sprite3.material.rotation = Math.PI / 3; + group2.add(sprite3); + + window.addEventListener('resize', onWindowResize); + document.addEventListener('pointermove', onPointerMove); +} + +function animate() { + renderer.render(scene, camera); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onPointerMove(event) { + if (selectedObject) { + selectedObject.material.color.set('#69f'); + selectedObject = null; + } + + pointer.x = (event.clientX / window.innerWidth) * 2 - 1; + pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; + + raycaster.setFromCamera(pointer, camera); + + const intersects = raycaster.intersectObject(group, true); + + if (intersects.length > 0) { + const res = intersects.filter(function (res) { + return res && res.object; + })[0]; + + if (res && res.object) { + selectedObject = res.object; + selectedObject.material.color.set('#f00'); + } + } +} diff --git a/examples-testing/examples/webgl_raycaster_texture.ts b/examples-testing/examples/webgl_raycaster_texture.ts new file mode 100644 index 000000000..72c7054dc --- /dev/null +++ b/examples-testing/examples/webgl_raycaster_texture.ts @@ -0,0 +1,286 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +const WRAPPING = { + RepeatWrapping: THREE.RepeatWrapping, + ClampToEdgeWrapping: THREE.ClampToEdgeWrapping, + MirroredRepeatWrapping: THREE.MirroredRepeatWrapping, +}; + +const params = { + wrapS: THREE.RepeatWrapping, + wrapT: THREE.RepeatWrapping, + offsetX: 0, + offsetY: 0, + repeatX: 1, + repeatY: 1, + rotation: 0, +}; + +function CanvasTexture(parentTexture) { + this._canvas = document.createElement('canvas'); + this._canvas.width = this._canvas.height = 1024; + this._context2D = this._canvas.getContext('2d'); + + if (parentTexture) { + this._parentTexture.push(parentTexture); + parentTexture.image = this._canvas; + } + + const that = this; + this._background = document.createElement('img'); + this._background.addEventListener('load', function () { + that._canvas.width = that._background.naturalWidth; + that._canvas.height = that._background.naturalHeight; + + that._crossRadius = Math.ceil(Math.min(that._canvas.width, that._canvas.height / 30)); + that._crossMax = Math.ceil(0.70710678 * that._crossRadius); + that._crossMin = Math.ceil(that._crossMax / 10); + that._crossThickness = Math.ceil(that._crossMax / 10); + + that._draw(); + }); + this._background.crossOrigin = ''; + this._background.src = 'textures/uv_grid_opengl.jpg'; + + this._draw(); +} + +CanvasTexture.prototype = { + constructor: CanvasTexture, + + _canvas: null, + _context2D: null, + _xCross: 0, + _yCross: 0, + + _crossRadius: 57, + _crossMax: 40, + _crossMin: 4, + _crossThickness: 4, + + _parentTexture: [], + + addParent: function (parentTexture) { + if (this._parentTexture.indexOf(parentTexture) === -1) { + this._parentTexture.push(parentTexture); + parentTexture.image = this._canvas; + } + }, + + setCrossPosition: function (x, y) { + this._xCross = x * this._canvas.width; + this._yCross = y * this._canvas.height; + + this._draw(); + }, + + _draw: function () { + if (!this._context2D) return; + + this._context2D.clearRect(0, 0, this._canvas.width, this._canvas.height); + + // Background. + this._context2D.drawImage(this._background, 0, 0); + + // Yellow cross. + this._context2D.lineWidth = this._crossThickness * 3; + this._context2D.strokeStyle = '#FFFF00'; + + this._context2D.beginPath(); + this._context2D.moveTo(this._xCross - this._crossMax - 2, this._yCross - this._crossMax - 2); + this._context2D.lineTo(this._xCross - this._crossMin, this._yCross - this._crossMin); + + this._context2D.moveTo(this._xCross + this._crossMin, this._yCross + this._crossMin); + this._context2D.lineTo(this._xCross + this._crossMax + 2, this._yCross + this._crossMax + 2); + + this._context2D.moveTo(this._xCross - this._crossMax - 2, this._yCross + this._crossMax + 2); + this._context2D.lineTo(this._xCross - this._crossMin, this._yCross + this._crossMin); + + this._context2D.moveTo(this._xCross + this._crossMin, this._yCross - this._crossMin); + this._context2D.lineTo(this._xCross + this._crossMax + 2, this._yCross - this._crossMax - 2); + + this._context2D.stroke(); + + for (let i = 0; i < this._parentTexture.length; i++) { + this._parentTexture[i].needsUpdate = true; + } + }, +}; + +const width = window.innerWidth; +const height = window.innerHeight; + +let canvas; +let planeTexture, cubeTexture, circleTexture; + +let container; + +let camera, scene, renderer; + +const raycaster = new THREE.Raycaster(); +const mouse = new THREE.Vector2(); +const onClickPosition = new THREE.Vector2(); + +init(); + +function init() { + container = document.getElementById('container'); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xeeeeee); + + camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000); + camera.position.x = -30; + camera.position.y = 40; + camera.position.z = 50; + camera.lookAt(scene.position); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(width, height); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // A cube, in the middle. + cubeTexture = new THREE.Texture(undefined, THREE.UVMapping, THREE.RepeatWrapping, THREE.RepeatWrapping); + cubeTexture.colorSpace = THREE.SRGBColorSpace; + canvas = new CanvasTexture(cubeTexture); + const cubeMaterial = new THREE.MeshBasicMaterial({ map: cubeTexture }); + const cubeGeometry = new THREE.BoxGeometry(20, 20, 20); + let uvs = cubeGeometry.attributes.uv.array; + // Set a specific texture mapping. + for (let i = 0; i < uvs.length; i++) { + uvs[i] *= 2; + } + + const cube = new THREE.Mesh(cubeGeometry, cubeMaterial); + cube.position.x = 4; + cube.position.y = -5; + cube.position.z = 0; + scene.add(cube); + + // A plane on the left + + planeTexture = new THREE.Texture( + undefined, + THREE.UVMapping, + THREE.MirroredRepeatWrapping, + THREE.MirroredRepeatWrapping, + ); + planeTexture.colorSpace = THREE.SRGBColorSpace; + canvas.addParent(planeTexture); + const planeMaterial = new THREE.MeshBasicMaterial({ map: planeTexture }); + const planeGeometry = new THREE.PlaneGeometry(25, 25, 1, 1); + uvs = planeGeometry.attributes.uv.array; + + // Set a specific texture mapping. + + for (let i = 0; i < uvs.length; i++) { + uvs[i] *= 2; + } + + const plane = new THREE.Mesh(planeGeometry, planeMaterial); + plane.position.x = -16; + plane.position.y = -5; + plane.position.z = 0; + scene.add(plane); + + // A circle on the right. + + circleTexture = new THREE.Texture(undefined, THREE.UVMapping, THREE.RepeatWrapping, THREE.RepeatWrapping); + circleTexture.colorSpace = THREE.SRGBColorSpace; + canvas.addParent(circleTexture); + const circleMaterial = new THREE.MeshBasicMaterial({ map: circleTexture }); + const circleGeometry = new THREE.CircleGeometry(25, 40, 0, Math.PI * 2); + uvs = circleGeometry.attributes.uv.array; + + // Set a specific texture mapping. + + for (let i = 0; i < uvs.length; i++) { + uvs[i] = (uvs[i] - 0.25) * 2; + } + + const circle = new THREE.Mesh(circleGeometry, circleMaterial); + circle.position.x = 24; + circle.position.y = -5; + circle.position.z = 0; + scene.add(circle); + + window.addEventListener('resize', onWindowResize); + container.addEventListener('mousemove', onMouseMove); + + // + + const gui = new GUI(); + gui.title('Circle Texture Settings'); + + gui.add(params, 'wrapS', WRAPPING).onChange(setwrapS); + gui.add(params, 'wrapT', WRAPPING).onChange(setwrapT); + gui.add(params, 'offsetX', 0, 5); + gui.add(params, 'offsetY', 0, 5); + gui.add(params, 'repeatX', 0, 5); + gui.add(params, 'repeatY', 0, 5); + gui.add(params, 'rotation', 0, 2 * Math.PI); + gui.open(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onMouseMove(evt) { + evt.preventDefault(); + + const array = getMousePosition(container, evt.clientX, evt.clientY); + onClickPosition.fromArray(array); + + const intersects = getIntersects(onClickPosition, scene.children); + + if (intersects.length > 0 && intersects[0].uv) { + const uv = intersects[0].uv; + intersects[0].object.material.map.transformUv(uv); + canvas.setCrossPosition(uv.x, uv.y); + } +} + +function getMousePosition(dom, x, y) { + const rect = dom.getBoundingClientRect(); + return [(x - rect.left) / rect.width, (y - rect.top) / rect.height]; +} + +function getIntersects(point, objects) { + mouse.set(point.x * 2 - 1, -(point.y * 2) + 1); + + raycaster.setFromCamera(mouse, camera); + + return raycaster.intersectObjects(objects, false); +} + +function animate() { + // update texture parameters + + circleTexture.offset.x = params.offsetX; + circleTexture.offset.y = params.offsetY; + circleTexture.repeat.x = params.repeatX; + circleTexture.repeat.y = params.repeatY; + circleTexture.rotation = params.rotation; + + // + + renderer.render(scene, camera); +} + +function setwrapS(value) { + circleTexture.wrapS = value; + circleTexture.needsUpdate = true; +} + +function setwrapT(value) { + circleTexture.wrapT = value; + circleTexture.needsUpdate = true; +} diff --git a/examples-testing/examples/webgl_raymarching_reflect.ts b/examples-testing/examples/webgl_raymarching_reflect.ts new file mode 100644 index 000000000..e5448ebbd --- /dev/null +++ b/examples-testing/examples/webgl_raymarching_reflect.ts @@ -0,0 +1,95 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let dolly, camera, scene, renderer; +let geometry, material, mesh; +let stats, clock; + +const canvas = document.querySelector('#canvas'); + +const config = { + saveImage: function () { + renderer.render(scene, camera); + window.open(canvas.toDataURL()); + }, + resolution: '512', +}; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer({ canvas: canvas }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(parseInt(config.resolution), parseInt(config.resolution)); + renderer.setAnimationLoop(animate); + + window.addEventListener('resize', onWindowResize); + + // THREE.Scene + scene = new THREE.Scene(); + + dolly = new THREE.Group(); + scene.add(dolly); + + clock = new THREE.Clock(); + + camera = new THREE.PerspectiveCamera(60, canvas.width / canvas.height, 1, 2000); + camera.position.z = 4; + dolly.add(camera); + + geometry = new THREE.PlaneGeometry(2.0, 2.0); + material = new THREE.RawShaderMaterial({ + uniforms: { + resolution: { value: new THREE.Vector2(canvas.width, canvas.height) }, + cameraWorldMatrix: { value: camera.matrixWorld }, + cameraProjectionMatrixInverse: { value: camera.projectionMatrixInverse.clone() }, + }, + vertexShader: document.getElementById('vertex_shader').textContent, + fragmentShader: document.getElementById('fragment_shader').textContent, + }); + mesh = new THREE.Mesh(geometry, material); + mesh.frustumCulled = false; + scene.add(mesh); + + // Controls + const controls = new OrbitControls(camera, canvas); + controls.enableZoom = false; + + // GUI + const gui = new GUI(); + gui.add(config, 'saveImage').name('Save Image'); + gui.add(config, 'resolution', ['256', '512', '800', 'full']).name('Resolution').onChange(onWindowResize); + + stats = new Stats(); + document.body.appendChild(stats.dom); +} + +function onWindowResize() { + if (config.resolution === 'full') { + renderer.setSize(window.innerWidth, window.innerHeight); + } else { + renderer.setSize(parseInt(config.resolution), parseInt(config.resolution)); + } + + camera.aspect = canvas.width / canvas.height; + camera.updateProjectionMatrix(); + + material.uniforms.resolution.value.set(canvas.width, canvas.height); + material.uniforms.cameraProjectionMatrixInverse.value.copy(camera.projectionMatrixInverse); +} + +function animate() { + stats.begin(); + + const elapsedTime = clock.getElapsedTime(); + + dolly.position.z = -elapsedTime; + + renderer.render(scene, camera); + + stats.end(); +} diff --git a/examples-testing/examples/webgl_read_float_buffer.ts b/examples-testing/examples/webgl_read_float_buffer.ts new file mode 100644 index 000000000..68452a12a --- /dev/null +++ b/examples-testing/examples/webgl_read_float_buffer.ts @@ -0,0 +1,153 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; + +let cameraRTT, sceneRTT, sceneScreen, renderer, zmesh1, zmesh2; + +let mouseX = 0, + mouseY = 0; + +const windowHalfX = window.innerWidth / 2; +const windowHalfY = window.innerHeight / 2; + +let rtTexture, material, quad; + +let delta = 0.01; +let valueNode; + +init(); + +function init() { + container = document.getElementById('container'); + + cameraRTT = new THREE.OrthographicCamera( + window.innerWidth / -2, + window.innerWidth / 2, + window.innerHeight / 2, + window.innerHeight / -2, + -10000, + 10000, + ); + cameraRTT.position.z = 100; + + // + + sceneRTT = new THREE.Scene(); + sceneScreen = new THREE.Scene(); + + let light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0, 0, 1).normalize(); + sceneRTT.add(light); + + light = new THREE.DirectionalLight(0xffd5d5, 4.5); + light.position.set(0, 0, -1).normalize(); + sceneRTT.add(light); + + rtTexture = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, { + minFilter: THREE.LinearFilter, + magFilter: THREE.NearestFilter, + format: THREE.RGBAFormat, + type: THREE.FloatType, + }); + + material = new THREE.ShaderMaterial({ + uniforms: { time: { value: 0.0 } }, + vertexShader: document.getElementById('vertexShader').textContent, + fragmentShader: document.getElementById('fragment_shader_pass_1').textContent, + }); + + const materialScreen = new THREE.ShaderMaterial({ + uniforms: { tDiffuse: { value: rtTexture.texture } }, + vertexShader: document.getElementById('vertexShader').textContent, + fragmentShader: document.getElementById('fragment_shader_screen').textContent, + + depthWrite: false, + }); + + const plane = new THREE.PlaneGeometry(window.innerWidth, window.innerHeight); + + quad = new THREE.Mesh(plane, material); + quad.position.z = -100; + sceneRTT.add(quad); + + const geometry = new THREE.TorusGeometry(100, 25, 15, 30); + + const mat1 = new THREE.MeshPhongMaterial({ color: 0x9c9c9c, specular: 0xffaa00, shininess: 5 }); + const mat2 = new THREE.MeshPhongMaterial({ color: 0x9c0000, specular: 0xff2200, shininess: 5 }); + + zmesh1 = new THREE.Mesh(geometry, mat1); + zmesh1.position.set(0, 0, 100); + zmesh1.scale.set(1.5, 1.5, 1.5); + sceneRTT.add(zmesh1); + + zmesh2 = new THREE.Mesh(geometry, mat2); + zmesh2.position.set(0, 150, 100); + zmesh2.scale.set(0.75, 0.75, 0.75); + sceneRTT.add(zmesh2); + + quad = new THREE.Mesh(plane, materialScreen); + quad.position.z = -100; + sceneScreen.add(quad); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.autoClear = false; + + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + valueNode = document.getElementById('values'); + + document.addEventListener('mousemove', onDocumentMouseMove); +} + +function onDocumentMouseMove(event) { + mouseX = event.clientX - windowHalfX; + mouseY = event.clientY - windowHalfY; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + const time = Date.now() * 0.0015; + + if (zmesh1 && zmesh2) { + zmesh1.rotation.y = -time; + zmesh2.rotation.y = -time + Math.PI / 2; + } + + if (material.uniforms['time'].value > 1 || material.uniforms['time'].value < 0) { + delta *= -1; + } + + material.uniforms['time'].value += delta; + + renderer.clear(); + + // Render first scene into texture + + renderer.setRenderTarget(rtTexture); + renderer.clear(); + renderer.render(sceneRTT, cameraRTT); + + // Render full screen quad with generated texture + + renderer.setRenderTarget(null); + renderer.render(sceneScreen, cameraRTT); + + const read = new Float32Array(4); + renderer.readRenderTargetPixels(rtTexture, windowHalfX + mouseX, windowHalfY - mouseY, 1, 1, read); + + valueNode.innerHTML = 'r:' + read[0] + '
g:' + read[1] + '
b:' + read[2]; +} diff --git a/examples-testing/examples/webgl_refraction.ts b/examples-testing/examples/webgl_refraction.ts new file mode 100644 index 000000000..572575afa --- /dev/null +++ b/examples-testing/examples/webgl_refraction.ts @@ -0,0 +1,135 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { Refractor } from 'three/addons/objects/Refractor.js'; +import { WaterRefractionShader } from 'three/addons/shaders/WaterRefractionShader.js'; + +let camera, scene, renderer, clock; + +let refractor, smallSphere; + +init(); + +async function init() { + const container = document.getElementById('container'); + + clock = new THREE.Clock(); + + // scene + scene = new THREE.Scene(); + + // camera + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 500); + camera.position.set(0, 75, 160); + + // refractor + + const refractorGeometry = new THREE.PlaneGeometry(90, 90); + + refractor = new Refractor(refractorGeometry, { + color: 0xcbcbcb, + textureWidth: 1024, + textureHeight: 1024, + shader: WaterRefractionShader, + }); + + refractor.position.set(0, 50, 0); + + scene.add(refractor); + + // load dudv map for distortion effect + + const loader = new THREE.TextureLoader(); + const dudvMap = await loader.loadAsync('textures/waterdudv.jpg'); + + dudvMap.wrapS = dudvMap.wrapT = THREE.RepeatWrapping; + refractor.material.uniforms.tDudv.value = dudvMap; + + // + + const geometry = new THREE.IcosahedronGeometry(5, 0); + const material = new THREE.MeshPhongMaterial({ color: 0xffffff, emissive: 0x333333, flatShading: true }); + smallSphere = new THREE.Mesh(geometry, material); + scene.add(smallSphere); + + // walls + const planeGeo = new THREE.PlaneGeometry(100.1, 100.1); + + const planeTop = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0xffffff })); + planeTop.position.y = 100; + planeTop.rotateX(Math.PI / 2); + scene.add(planeTop); + + const planeBottom = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0xffffff })); + planeBottom.rotateX(-Math.PI / 2); + scene.add(planeBottom); + + const planeBack = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0x7f7fff })); + planeBack.position.z = -50; + planeBack.position.y = 50; + scene.add(planeBack); + + const planeRight = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0x00ff00 })); + planeRight.position.x = 50; + planeRight.position.y = 50; + planeRight.rotateY(-Math.PI / 2); + scene.add(planeRight); + + const planeLeft = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0xff0000 })); + planeLeft.position.x = -50; + planeLeft.position.y = 50; + planeLeft.rotateY(Math.PI / 2); + scene.add(planeLeft); + + // lights + const mainLight = new THREE.PointLight(0xe7e7e7, 2.5, 250, 0); + mainLight.position.y = 60; + scene.add(mainLight); + + const greenLight = new THREE.PointLight(0x00ff00, 0.5, 1000, 0); + greenLight.position.set(550, 50, 0); + scene.add(greenLight); + + const redLight = new THREE.PointLight(0xff0000, 0.5, 1000, 0); + redLight.position.set(-550, 50, 0); + scene.add(redLight); + + const blueLight = new THREE.PointLight(0xbbbbfe, 0.5, 1000, 0); + blueLight.position.set(0, 50, 550); + scene.add(blueLight); + + // renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // controls + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 40, 0); + controls.maxDistance = 400; + controls.minDistance = 10; + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const time = clock.getElapsedTime(); + + refractor.material.uniforms.time.value = time; + + smallSphere.position.set(Math.cos(time) * 30, Math.abs(Math.cos(time * 2)) * 20 + 5, Math.sin(time) * 30); + smallSphere.rotation.y = Math.PI / 2 - time; + smallSphere.rotation.z = time * 8; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_rtt.ts b/examples-testing/examples/webgl_rtt.ts new file mode 100644 index 000000000..9f16fdab8 --- /dev/null +++ b/examples-testing/examples/webgl_rtt.ts @@ -0,0 +1,171 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; + +let cameraRTT, camera, sceneRTT, sceneScreen, scene, renderer, zmesh1, zmesh2; + +let mouseX = 0, + mouseY = 0; + +const windowHalfX = window.innerWidth / 2; +const windowHalfY = window.innerHeight / 2; + +let rtTexture, material, quad; + +let delta = 0.01; + +init(); + +function init() { + container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 100; + + cameraRTT = new THREE.OrthographicCamera( + window.innerWidth / -2, + window.innerWidth / 2, + window.innerHeight / 2, + window.innerHeight / -2, + -10000, + 10000, + ); + cameraRTT.position.z = 100; + + // + + scene = new THREE.Scene(); + sceneRTT = new THREE.Scene(); + sceneScreen = new THREE.Scene(); + + let light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0, 0, 1).normalize(); + sceneRTT.add(light); + + light = new THREE.DirectionalLight(0xffd5d5, 4.5); + light.position.set(0, 0, -1).normalize(); + sceneRTT.add(light); + + rtTexture = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight); + + material = new THREE.ShaderMaterial({ + uniforms: { time: { value: 0.0 } }, + vertexShader: document.getElementById('vertexShader').textContent, + fragmentShader: document.getElementById('fragment_shader_pass_1').textContent, + }); + + const materialScreen = new THREE.ShaderMaterial({ + uniforms: { tDiffuse: { value: rtTexture.texture } }, + vertexShader: document.getElementById('vertexShader').textContent, + fragmentShader: document.getElementById('fragment_shader_screen').textContent, + + depthWrite: false, + }); + + const plane = new THREE.PlaneGeometry(window.innerWidth, window.innerHeight); + + quad = new THREE.Mesh(plane, material); + quad.position.z = -100; + sceneRTT.add(quad); + + const torusGeometry = new THREE.TorusGeometry(100, 25, 15, 30); + + const mat1 = new THREE.MeshPhongMaterial({ color: 0x9c9c9c, specular: 0xffaa00, shininess: 5 }); + const mat2 = new THREE.MeshPhongMaterial({ color: 0x9c0000, specular: 0xff2200, shininess: 5 }); + + zmesh1 = new THREE.Mesh(torusGeometry, mat1); + zmesh1.position.set(0, 0, 100); + zmesh1.scale.set(1.5, 1.5, 1.5); + sceneRTT.add(zmesh1); + + zmesh2 = new THREE.Mesh(torusGeometry, mat2); + zmesh2.position.set(0, 150, 100); + zmesh2.scale.set(0.75, 0.75, 0.75); + sceneRTT.add(zmesh2); + + quad = new THREE.Mesh(plane, materialScreen); + quad.position.z = -100; + sceneScreen.add(quad); + + const n = 5, + geometry = new THREE.SphereGeometry(10, 64, 32), + material2 = new THREE.MeshBasicMaterial({ color: 0xffffff, map: rtTexture.texture }); + + for (let j = 0; j < n; j++) { + for (let i = 0; i < n; i++) { + const mesh = new THREE.Mesh(geometry, material2); + + mesh.position.x = (i - (n - 1) / 2) * 20; + mesh.position.y = (j - (n - 1) / 2) * 20; + mesh.position.z = 0; + + mesh.rotation.y = -Math.PI / 2; + + scene.add(mesh); + } + } + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.autoClear = false; + + container.appendChild(renderer.domElement); + + stats = new Stats(); + container.appendChild(stats.dom); + + document.addEventListener('mousemove', onDocumentMouseMove); +} + +function onDocumentMouseMove(event) { + mouseX = event.clientX - windowHalfX; + mouseY = event.clientY - windowHalfY; +} + +// + +function animate() { + render(); + stats.update(); +} + +function render() { + const time = Date.now() * 0.0015; + + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-mouseY - camera.position.y) * 0.05; + + camera.lookAt(scene.position); + + if (zmesh1 && zmesh2) { + zmesh1.rotation.y = -time; + zmesh2.rotation.y = -time + Math.PI / 2; + } + + if (material.uniforms['time'].value > 1 || material.uniforms['time'].value < 0) { + delta *= -1; + } + + material.uniforms['time'].value += delta; + + // Render first scene into texture + + renderer.setRenderTarget(rtTexture); + renderer.clear(); + renderer.render(sceneRTT, cameraRTT); + + // Render full screen quad with generated texture + + renderer.setRenderTarget(null); + renderer.clear(); + renderer.render(sceneScreen, cameraRTT); + + // Render second scene to screen + // (using first scene as regular texture) + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_shader.ts b/examples-testing/examples/webgl_shader.ts new file mode 100644 index 000000000..47a6c7ece --- /dev/null +++ b/examples-testing/examples/webgl_shader.ts @@ -0,0 +1,50 @@ +import * as THREE from 'three'; + +let camera, scene, renderer; + +let uniforms; + +init(); + +function init() { + const container = document.getElementById('container'); + + camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); + + scene = new THREE.Scene(); + + const geometry = new THREE.PlaneGeometry(2, 2); + + uniforms = { + time: { value: 1.0 }, + }; + + const material = new THREE.ShaderMaterial({ + uniforms: uniforms, + vertexShader: document.getElementById('vertexShader').textContent, + fragmentShader: document.getElementById('fragmentShader').textContent, + }); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + uniforms['time'].value = performance.now() / 1000; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_shader_lava.ts b/examples-testing/examples/webgl_shader_lava.ts new file mode 100644 index 000000000..973a580eb --- /dev/null +++ b/examples-testing/examples/webgl_shader_lava.ts @@ -0,0 +1,101 @@ +import * as THREE from 'three'; + +import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; +import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; +import { BloomPass } from 'three/addons/postprocessing/BloomPass.js'; +import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; + +let camera, renderer, composer, clock; + +let uniforms, mesh; + +init(); + +function init() { + const container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 3000); + camera.position.z = 4; + + const scene = new THREE.Scene(); + + clock = new THREE.Clock(); + + const textureLoader = new THREE.TextureLoader(); + + const cloudTexture = textureLoader.load('textures/lava/cloud.png'); + const lavaTexture = textureLoader.load('textures/lava/lavatile.jpg'); + + lavaTexture.colorSpace = THREE.SRGBColorSpace; + + cloudTexture.wrapS = cloudTexture.wrapT = THREE.RepeatWrapping; + lavaTexture.wrapS = lavaTexture.wrapT = THREE.RepeatWrapping; + + uniforms = { + fogDensity: { value: 0.45 }, + fogColor: { value: new THREE.Vector3(0, 0, 0) }, + time: { value: 1.0 }, + uvScale: { value: new THREE.Vector2(3.0, 1.0) }, + texture1: { value: cloudTexture }, + texture2: { value: lavaTexture }, + }; + + const size = 0.65; + + const material = new THREE.ShaderMaterial({ + uniforms: uniforms, + vertexShader: document.getElementById('vertexShader').textContent, + fragmentShader: document.getElementById('fragmentShader').textContent, + }); + + mesh = new THREE.Mesh(new THREE.TorusGeometry(size, 0.3, 30, 30), material); + mesh.rotation.x = 0.3; + scene.add(mesh); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.autoClear = false; + container.appendChild(renderer.domElement); + + // + + const renderModel = new RenderPass(scene, camera); + const effectBloom = new BloomPass(1.25); + const outputPass = new OutputPass(); + + composer = new EffectComposer(renderer); + + composer.addPass(renderModel); + composer.addPass(effectBloom); + composer.addPass(outputPass); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const delta = 5 * clock.getDelta(); + + uniforms['time'].value += 0.2 * delta; + + mesh.rotation.y += 0.0125 * delta; + mesh.rotation.x += 0.05 * delta; + + renderer.clear(); + composer.render(0.01); +} diff --git a/examples-testing/examples/webgl_shaders_ocean.ts b/examples-testing/examples/webgl_shaders_ocean.ts new file mode 100644 index 000000000..8b0f9a738 --- /dev/null +++ b/examples-testing/examples/webgl_shaders_ocean.ts @@ -0,0 +1,169 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { Water } from 'three/addons/objects/Water.js'; +import { Sky } from 'three/addons/objects/Sky.js'; + +let container, stats; +let camera, scene, renderer; +let controls, water, sun, mesh; + +init(); + +function init() { + container = document.getElementById('container'); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 0.5; + container.appendChild(renderer.domElement); + + // + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 1, 20000); + camera.position.set(30, 30, 100); + + // + + sun = new THREE.Vector3(); + + // Water + + const waterGeometry = new THREE.PlaneGeometry(10000, 10000); + + water = new Water(waterGeometry, { + textureWidth: 512, + textureHeight: 512, + waterNormals: new THREE.TextureLoader().load('textures/waternormals.jpg', function (texture) { + texture.wrapS = texture.wrapT = THREE.RepeatWrapping; + }), + sunDirection: new THREE.Vector3(), + sunColor: 0xffffff, + waterColor: 0x001e0f, + distortionScale: 3.7, + fog: scene.fog !== undefined, + }); + + water.rotation.x = -Math.PI / 2; + + scene.add(water); + + // Skybox + + const sky = new Sky(); + sky.scale.setScalar(10000); + scene.add(sky); + + const skyUniforms = sky.material.uniforms; + + skyUniforms['turbidity'].value = 10; + skyUniforms['rayleigh'].value = 2; + skyUniforms['mieCoefficient'].value = 0.005; + skyUniforms['mieDirectionalG'].value = 0.8; + + const parameters = { + elevation: 2, + azimuth: 180, + }; + + const pmremGenerator = new THREE.PMREMGenerator(renderer); + const sceneEnv = new THREE.Scene(); + + let renderTarget; + + function updateSun() { + const phi = THREE.MathUtils.degToRad(90 - parameters.elevation); + const theta = THREE.MathUtils.degToRad(parameters.azimuth); + + sun.setFromSphericalCoords(1, phi, theta); + + sky.material.uniforms['sunPosition'].value.copy(sun); + water.material.uniforms['sunDirection'].value.copy(sun).normalize(); + + if (renderTarget !== undefined) renderTarget.dispose(); + + sceneEnv.add(sky); + renderTarget = pmremGenerator.fromScene(sceneEnv); + scene.add(sky); + + scene.environment = renderTarget.texture; + } + + updateSun(); + + // + + const geometry = new THREE.BoxGeometry(30, 30, 30); + const material = new THREE.MeshStandardMaterial({ roughness: 0 }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + controls = new OrbitControls(camera, renderer.domElement); + controls.maxPolarAngle = Math.PI * 0.495; + controls.target.set(0, 10, 0); + controls.minDistance = 40.0; + controls.maxDistance = 200.0; + controls.update(); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // GUI + + const gui = new GUI(); + + const folderSky = gui.addFolder('Sky'); + folderSky.add(parameters, 'elevation', 0, 90, 0.1).onChange(updateSun); + folderSky.add(parameters, 'azimuth', -180, 180, 0.1).onChange(updateSun); + folderSky.open(); + + const waterUniforms = water.material.uniforms; + + const folderWater = gui.addFolder('Water'); + folderWater.add(waterUniforms.distortionScale, 'value', 0, 8, 0.1).name('distortionScale'); + folderWater.add(waterUniforms.size, 'value', 0.1, 10, 0.1).name('size'); + folderWater.open(); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + const time = performance.now() * 0.001; + + mesh.position.y = Math.sin(time) * 20 + 5; + mesh.rotation.x = time * 0.5; + mesh.rotation.z = time * 0.51; + + water.material.uniforms['time'].value += 1.0 / 60.0; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_shaders_sky.ts b/examples-testing/examples/webgl_shaders_sky.ts new file mode 100644 index 000000000..18020f78f --- /dev/null +++ b/examples-testing/examples/webgl_shaders_sky.ts @@ -0,0 +1,103 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { Sky } from 'three/addons/objects/Sky.js'; + +let camera, scene, renderer; + +let sky, sun; + +init(); +render(); + +function initSky() { + // Add Sky + sky = new Sky(); + sky.scale.setScalar(450000); + scene.add(sky); + + sun = new THREE.Vector3(); + + /// GUI + + const effectController = { + turbidity: 10, + rayleigh: 3, + mieCoefficient: 0.005, + mieDirectionalG: 0.7, + elevation: 2, + azimuth: 180, + exposure: renderer.toneMappingExposure, + }; + + function guiChanged() { + const uniforms = sky.material.uniforms; + uniforms['turbidity'].value = effectController.turbidity; + uniforms['rayleigh'].value = effectController.rayleigh; + uniforms['mieCoefficient'].value = effectController.mieCoefficient; + uniforms['mieDirectionalG'].value = effectController.mieDirectionalG; + + const phi = THREE.MathUtils.degToRad(90 - effectController.elevation); + const theta = THREE.MathUtils.degToRad(effectController.azimuth); + + sun.setFromSphericalCoords(1, phi, theta); + + uniforms['sunPosition'].value.copy(sun); + + renderer.toneMappingExposure = effectController.exposure; + renderer.render(scene, camera); + } + + const gui = new GUI(); + + gui.add(effectController, 'turbidity', 0.0, 20.0, 0.1).onChange(guiChanged); + gui.add(effectController, 'rayleigh', 0.0, 4, 0.001).onChange(guiChanged); + gui.add(effectController, 'mieCoefficient', 0.0, 0.1, 0.001).onChange(guiChanged); + gui.add(effectController, 'mieDirectionalG', 0.0, 1, 0.001).onChange(guiChanged); + gui.add(effectController, 'elevation', 0, 90, 0.1).onChange(guiChanged); + gui.add(effectController, 'azimuth', -180, 180, 0.1).onChange(guiChanged); + gui.add(effectController, 'exposure', 0, 1, 0.0001).onChange(guiChanged); + + guiChanged(); +} + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 100, 2000000); + camera.position.set(0, 100, 2000); + + scene = new THREE.Scene(); + + const helper = new THREE.GridHelper(10000, 2, 0xffffff, 0xffffff); + scene.add(helper); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 0.5; + document.body.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); + //controls.maxPolarAngle = Math.PI / 2; + controls.enableZoom = false; + controls.enablePan = false; + + initSky(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_shadow_contact.ts b/examples-testing/examples/webgl_shadow_contact.ts new file mode 100644 index 000000000..9eda35b83 --- /dev/null +++ b/examples-testing/examples/webgl_shadow_contact.ts @@ -0,0 +1,272 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { HorizontalBlurShader } from 'three/addons/shaders/HorizontalBlurShader.js'; +import { VerticalBlurShader } from 'three/addons/shaders/VerticalBlurShader.js'; + +let camera, scene, renderer, stats, gui; + +const meshes = []; + +const PLANE_WIDTH = 2.5; +const PLANE_HEIGHT = 2.5; +const CAMERA_HEIGHT = 0.3; + +const state = { + shadow: { + blur: 3.5, + darkness: 1, + opacity: 1, + }, + plane: { + color: '#ffffff', + opacity: 1, + }, + showWireframe: false, +}; + +let shadowGroup, + renderTarget, + renderTargetBlur, + shadowCamera, + cameraHelper, + depthMaterial, + horizontalBlurMaterial, + verticalBlurMaterial; + +let plane, blurPlane, fillPlane; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0.5, 1, 2); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); + + // add the example meshes + + const geometries = [ + new THREE.BoxGeometry(0.4, 0.4, 0.4), + new THREE.IcosahedronGeometry(0.3), + new THREE.TorusKnotGeometry(0.4, 0.05, 256, 24, 1, 3), + ]; + + const material = new THREE.MeshNormalMaterial(); + + for (let i = 0, l = geometries.length; i < l; i++) { + const angle = (i / l) * Math.PI * 2; + + const geometry = geometries[i]; + const mesh = new THREE.Mesh(geometry, material); + mesh.position.y = 0.1; + mesh.position.x = Math.cos(angle) / 2.0; + mesh.position.z = Math.sin(angle) / 2.0; + scene.add(mesh); + meshes.push(mesh); + } + + // the container, if you need to move the plane just move this + shadowGroup = new THREE.Group(); + shadowGroup.position.y = -0.3; + scene.add(shadowGroup); + + // the render target that will show the shadows in the plane texture + renderTarget = new THREE.WebGLRenderTarget(512, 512); + renderTarget.texture.generateMipmaps = false; + + // the render target that we will use to blur the first render target + renderTargetBlur = new THREE.WebGLRenderTarget(512, 512); + renderTargetBlur.texture.generateMipmaps = false; + + // make a plane and make it face up + const planeGeometry = new THREE.PlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT).rotateX(Math.PI / 2); + const planeMaterial = new THREE.MeshBasicMaterial({ + map: renderTarget.texture, + opacity: state.shadow.opacity, + transparent: true, + depthWrite: false, + }); + plane = new THREE.Mesh(planeGeometry, planeMaterial); + // make sure it's rendered after the fillPlane + plane.renderOrder = 1; + shadowGroup.add(plane); + + // the y from the texture is flipped! + plane.scale.y = -1; + + // the plane onto which to blur the texture + blurPlane = new THREE.Mesh(planeGeometry); + blurPlane.visible = false; + shadowGroup.add(blurPlane); + + // the plane with the color of the ground + const fillPlaneMaterial = new THREE.MeshBasicMaterial({ + color: state.plane.color, + opacity: state.plane.opacity, + transparent: true, + depthWrite: false, + }); + fillPlane = new THREE.Mesh(planeGeometry, fillPlaneMaterial); + fillPlane.rotateX(Math.PI); + shadowGroup.add(fillPlane); + + // the camera to render the depth material from + shadowCamera = new THREE.OrthographicCamera( + -PLANE_WIDTH / 2, + PLANE_WIDTH / 2, + PLANE_HEIGHT / 2, + -PLANE_HEIGHT / 2, + 0, + CAMERA_HEIGHT, + ); + shadowCamera.rotation.x = Math.PI / 2; // get the camera to look up + shadowGroup.add(shadowCamera); + + cameraHelper = new THREE.CameraHelper(shadowCamera); + + // like MeshDepthMaterial, but goes from black to transparent + depthMaterial = new THREE.MeshDepthMaterial(); + depthMaterial.userData.darkness = { value: state.shadow.darkness }; + depthMaterial.onBeforeCompile = function (shader) { + shader.uniforms.darkness = depthMaterial.userData.darkness; + shader.fragmentShader = /* glsl */ ` + uniform float darkness; + ${shader.fragmentShader.replace( + 'gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );', + 'gl_FragColor = vec4( vec3( 0.0 ), ( 1.0 - fragCoordZ ) * darkness );', + )} + `; + }; + + depthMaterial.depthTest = false; + depthMaterial.depthWrite = false; + + horizontalBlurMaterial = new THREE.ShaderMaterial(HorizontalBlurShader); + horizontalBlurMaterial.depthTest = false; + + verticalBlurMaterial = new THREE.ShaderMaterial(VerticalBlurShader); + verticalBlurMaterial.depthTest = false; + + // + + gui = new GUI(); + const shadowFolder = gui.addFolder('shadow'); + shadowFolder.open(); + const planeFolder = gui.addFolder('plane'); + planeFolder.open(); + + shadowFolder.add(state.shadow, 'blur', 0, 15, 0.1); + shadowFolder.add(state.shadow, 'darkness', 1, 5, 0.1).onChange(function () { + depthMaterial.userData.darkness.value = state.shadow.darkness; + }); + shadowFolder.add(state.shadow, 'opacity', 0, 1, 0.01).onChange(function () { + plane.material.opacity = state.shadow.opacity; + }); + planeFolder.addColor(state.plane, 'color').onChange(function () { + fillPlane.material.color = new THREE.Color(state.plane.color); + }); + planeFolder.add(state.plane, 'opacity', 0, 1, 0.01).onChange(function () { + fillPlane.material.opacity = state.plane.opacity; + }); + + gui.add(state, 'showWireframe').onChange(function () { + if (state.showWireframe) { + scene.add(cameraHelper); + } else { + scene.remove(cameraHelper); + } + }); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + new OrbitControls(camera, renderer.domElement); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// renderTarget --> blurPlane (horizontalBlur) --> renderTargetBlur --> blurPlane (verticalBlur) --> renderTarget +function blurShadow(amount) { + blurPlane.visible = true; + + // blur horizontally and draw in the renderTargetBlur + blurPlane.material = horizontalBlurMaterial; + blurPlane.material.uniforms.tDiffuse.value = renderTarget.texture; + horizontalBlurMaterial.uniforms.h.value = (amount * 1) / 256; + + renderer.setRenderTarget(renderTargetBlur); + renderer.render(blurPlane, shadowCamera); + + // blur vertically and draw in the main renderTarget + blurPlane.material = verticalBlurMaterial; + blurPlane.material.uniforms.tDiffuse.value = renderTargetBlur.texture; + verticalBlurMaterial.uniforms.v.value = (amount * 1) / 256; + + renderer.setRenderTarget(renderTarget); + renderer.render(blurPlane, shadowCamera); + + blurPlane.visible = false; +} + +function animate() { + meshes.forEach(mesh => { + mesh.rotation.x += 0.01; + mesh.rotation.y += 0.02; + }); + + // + + // remove the background + const initialBackground = scene.background; + scene.background = null; + + // force the depthMaterial to everything + cameraHelper.visible = false; + scene.overrideMaterial = depthMaterial; + + // set renderer clear alpha + const initialClearAlpha = renderer.getClearAlpha(); + renderer.setClearAlpha(0); + + // render to the render target to get the depths + renderer.setRenderTarget(renderTarget); + renderer.render(scene, shadowCamera); + + // and reset the override material + scene.overrideMaterial = null; + cameraHelper.visible = true; + + blurShadow(state.shadow.blur); + + // a second pass to reduce the artifacts + // (0.4 is the minimum blur amout so that the artifacts are gone) + blurShadow(state.shadow.blur * 0.4); + + // reset and render the normal scene + renderer.setRenderTarget(null); + renderer.setClearAlpha(initialClearAlpha); + scene.background = initialBackground; + + renderer.render(scene, camera); + stats.update(); +} diff --git a/examples-testing/examples/webgl_shadowmap.ts b/examples-testing/examples/webgl_shadowmap.ts new file mode 100644 index 000000000..6d0ac3adb --- /dev/null +++ b/examples-testing/examples/webgl_shadowmap.ts @@ -0,0 +1,311 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { FontLoader } from 'three/addons/loaders/FontLoader.js'; +import { TextGeometry } from 'three/addons/geometries/TextGeometry.js'; +import { ShadowMapViewer } from 'three/addons/utils/ShadowMapViewer.js'; + +const SHADOW_MAP_WIDTH = 2048, + SHADOW_MAP_HEIGHT = 1024; + +let SCREEN_WIDTH = window.innerWidth; +let SCREEN_HEIGHT = window.innerHeight; +const FLOOR = -250; + +let camera, controls, scene, renderer; +let container, stats; + +const NEAR = 10, + FAR = 3000; + +let mixer; + +const morphs = []; + +let light; +let lightShadowMapViewer; + +const clock = new THREE.Clock(); + +let showHUD = false; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + // CAMERA + + camera = new THREE.PerspectiveCamera(23, SCREEN_WIDTH / SCREEN_HEIGHT, NEAR, FAR); + camera.position.set(700, 50, 1900); + + // SCENE + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x59472b); + scene.fog = new THREE.Fog(0x59472b, 1000, FAR); + + // LIGHTS + + const ambient = new THREE.AmbientLight(0xffffff); + scene.add(ambient); + + light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0, 1500, 1000); + light.castShadow = true; + light.shadow.camera.top = 2000; + light.shadow.camera.bottom = -2000; + light.shadow.camera.left = -2000; + light.shadow.camera.right = 2000; + light.shadow.camera.near = 1200; + light.shadow.camera.far = 2500; + light.shadow.bias = 0.0001; + + light.shadow.mapSize.width = SHADOW_MAP_WIDTH; + light.shadow.mapSize.height = SHADOW_MAP_HEIGHT; + + scene.add(light); + + createHUD(); + createScene(); + + // RENDERER + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + renderer.autoClear = false; + + // + + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.PCFShadowMap; + + // CONTROLS + + controls = new FirstPersonControls(camera, renderer.domElement); + + controls.lookSpeed = 0.0125; + controls.movementSpeed = 500; + controls.lookVertical = true; + + controls.lookAt(scene.position); + + // STATS + + stats = new Stats(); + //container.appendChild( stats.dom ); + + // + + window.addEventListener('resize', onWindowResize); + window.addEventListener('keydown', onKeyDown); +} + +function onWindowResize() { + SCREEN_WIDTH = window.innerWidth; + SCREEN_HEIGHT = window.innerHeight; + + camera.aspect = SCREEN_WIDTH / SCREEN_HEIGHT; + camera.updateProjectionMatrix(); + + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + + controls.handleResize(); +} + +function onKeyDown(event) { + switch (event.keyCode) { + case 84 /*t*/: + showHUD = !showHUD; + break; + } +} + +function createHUD() { + lightShadowMapViewer = new ShadowMapViewer(light); + lightShadowMapViewer.position.x = 10; + lightShadowMapViewer.position.y = SCREEN_HEIGHT - SHADOW_MAP_HEIGHT / 4 - 10; + lightShadowMapViewer.size.width = SHADOW_MAP_WIDTH / 4; + lightShadowMapViewer.size.height = SHADOW_MAP_HEIGHT / 4; + lightShadowMapViewer.update(); +} + +function createScene() { + // GROUND + + const geometry = new THREE.PlaneGeometry(100, 100); + const planeMaterial = new THREE.MeshPhongMaterial({ color: 0xffdd99 }); + + const ground = new THREE.Mesh(geometry, planeMaterial); + + ground.position.set(0, FLOOR, 0); + ground.rotation.x = -Math.PI / 2; + ground.scale.set(100, 100, 100); + + ground.castShadow = false; + ground.receiveShadow = true; + + scene.add(ground); + + // TEXT + + const loader = new FontLoader(); + loader.load('fonts/helvetiker_bold.typeface.json', function (font) { + const textGeo = new TextGeometry('THREE.JS', { + font: font, + + size: 200, + depth: 50, + curveSegments: 12, + + bevelThickness: 2, + bevelSize: 5, + bevelEnabled: true, + }); + + textGeo.computeBoundingBox(); + const centerOffset = -0.5 * (textGeo.boundingBox.max.x - textGeo.boundingBox.min.x); + + const textMaterial = new THREE.MeshPhongMaterial({ color: 0xff0000, specular: 0xffffff }); + + const mesh = new THREE.Mesh(textGeo, textMaterial); + mesh.position.x = centerOffset; + mesh.position.y = FLOOR + 67; + + mesh.castShadow = true; + mesh.receiveShadow = true; + + scene.add(mesh); + }); + + // CUBES + + const cubes1 = new THREE.Mesh(new THREE.BoxGeometry(1500, 220, 150), planeMaterial); + + cubes1.position.y = FLOOR - 50; + cubes1.position.z = 20; + + cubes1.castShadow = true; + cubes1.receiveShadow = true; + + scene.add(cubes1); + + const cubes2 = new THREE.Mesh(new THREE.BoxGeometry(1600, 170, 250), planeMaterial); + + cubes2.position.y = FLOOR - 50; + cubes2.position.z = 20; + + cubes2.castShadow = true; + cubes2.receiveShadow = true; + + scene.add(cubes2); + + // MORPHS + + mixer = new THREE.AnimationMixer(scene); + + function addMorph(mesh, clip, speed, duration, x, y, z, fudgeColor) { + mesh = mesh.clone(); + mesh.material = mesh.material.clone(); + + if (fudgeColor) { + mesh.material.color.offsetHSL(0, Math.random() * 0.5 - 0.25, Math.random() * 0.5 - 0.25); + } + + mesh.speed = speed; + + mixer + .clipAction(clip, mesh) + .setDuration(duration) + // to shift the playback out of phase: + .startAt(-duration * Math.random()) + .play(); + + mesh.position.set(x, y, z); + mesh.rotation.y = Math.PI / 2; + + mesh.castShadow = true; + mesh.receiveShadow = true; + + scene.add(mesh); + + morphs.push(mesh); + } + + const gltfloader = new GLTFLoader(); + + gltfloader.load('models/gltf/Horse.glb', function (gltf) { + const mesh = gltf.scene.children[0]; + + const clip = gltf.animations[0]; + + addMorph(mesh, clip, 550, 1, 100 - Math.random() * 1000, FLOOR, 300, true); + addMorph(mesh, clip, 550, 1, 100 - Math.random() * 1000, FLOOR, 450, true); + addMorph(mesh, clip, 550, 1, 100 - Math.random() * 1000, FLOOR, 600, true); + + addMorph(mesh, clip, 550, 1, 100 - Math.random() * 1000, FLOOR, -300, true); + addMorph(mesh, clip, 550, 1, 100 - Math.random() * 1000, FLOOR, -450, true); + addMorph(mesh, clip, 550, 1, 100 - Math.random() * 1000, FLOOR, -600, true); + }); + + gltfloader.load('models/gltf/Flamingo.glb', function (gltf) { + const mesh = gltf.scene.children[0]; + const clip = gltf.animations[0]; + + addMorph(mesh, clip, 500, 1, 500 - Math.random() * 500, FLOOR + 350, 40); + }); + + gltfloader.load('models/gltf/Stork.glb', function (gltf) { + const mesh = gltf.scene.children[0]; + const clip = gltf.animations[0]; + + addMorph(mesh, clip, 350, 1, 500 - Math.random() * 500, FLOOR + 350, 340); + }); + + gltfloader.load('models/gltf/Parrot.glb', function (gltf) { + const mesh = gltf.scene.children[0]; + const clip = gltf.animations[0]; + + addMorph(mesh, clip, 450, 0.5, 500 - Math.random() * 500, FLOOR + 300, 700); + }); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + const delta = clock.getDelta(); + + mixer.update(delta); + + for (let i = 0; i < morphs.length; i++) { + const morph = morphs[i]; + + morph.position.x += morph.speed * delta; + + if (morph.position.x > 2000) { + morph.position.x = -1000 - Math.random() * 500; + } + } + + controls.update(delta); + + renderer.clear(); + renderer.render(scene, camera); + + // Render debug HUD with shadow map + + if (showHUD) { + lightShadowMapViewer.render(renderer); + } +} diff --git a/examples-testing/examples/webgl_shadowmap_csm.ts b/examples-testing/examples/webgl_shadowmap_csm.ts new file mode 100644 index 000000000..c89bc02df --- /dev/null +++ b/examples-testing/examples/webgl_shadowmap_csm.ts @@ -0,0 +1,253 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { CSM } from 'three/addons/csm/CSM.js'; +import { CSMHelper } from 'three/addons/csm/CSMHelper.js'; + +let renderer, scene, camera, orthoCamera, controls, csm, csmHelper; + +const params = { + orthographic: false, + fade: false, + shadows: true, + far: 1000, + mode: 'practical', + lightX: -1, + lightY: -1, + lightZ: -1, + margin: 100, + lightFar: 5000, + lightNear: 1, + autoUpdateHelper: true, + updateHelper: function () { + csmHelper.update(); + }, +}; + +init(); + +function updateOrthoCamera() { + const size = controls.target.distanceTo(camera.position); + const aspect = camera.aspect; + + orthoCamera.left = (size * aspect) / -2; + orthoCamera.right = (size * aspect) / 2; + + orthoCamera.top = size / 2; + orthoCamera.bottom = size / -2; + orthoCamera.position.copy(camera.position); + orthoCamera.rotation.copy(camera.rotation); + orthoCamera.updateProjectionMatrix(); +} + +function init() { + scene = new THREE.Scene(); + scene.background = new THREE.Color('#454e61'); + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 5000); + orthoCamera = new THREE.OrthographicCamera(); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + renderer.shadowMap.enabled = params.shadows; + renderer.shadowMap.type = THREE.PCFSoftShadowMap; + + controls = new OrbitControls(camera, renderer.domElement); + controls.maxPolarAngle = Math.PI / 2; + camera.position.set(60, 60, 0); + controls.target = new THREE.Vector3(-100, 10, 0); + controls.update(); + + const ambientLight = new THREE.AmbientLight(0xffffff, 1.5); + scene.add(ambientLight); + + const additionalDirectionalLight = new THREE.DirectionalLight(0x000020, 1.5); + additionalDirectionalLight.position + .set(params.lightX, params.lightY, params.lightZ) + .normalize() + .multiplyScalar(-200); + scene.add(additionalDirectionalLight); + + csm = new CSM({ + maxFar: params.far, + cascades: 4, + mode: params.mode, + parent: scene, + shadowMapSize: 1024, + lightDirection: new THREE.Vector3(params.lightX, params.lightY, params.lightZ).normalize(), + camera: camera, + }); + + csmHelper = new CSMHelper(csm); + csmHelper.visible = false; + scene.add(csmHelper); + + const floorMaterial = new THREE.MeshPhongMaterial({ color: '#252a34' }); + csm.setupMaterial(floorMaterial); + + const floor = new THREE.Mesh(new THREE.PlaneGeometry(10000, 10000, 8, 8), floorMaterial); + floor.rotation.x = -Math.PI / 2; + floor.castShadow = true; + floor.receiveShadow = true; + scene.add(floor); + + const material1 = new THREE.MeshPhongMaterial({ color: '#08d9d6' }); + csm.setupMaterial(material1); + + const material2 = new THREE.MeshPhongMaterial({ color: '#ff2e63' }); + csm.setupMaterial(material2); + + const geometry = new THREE.BoxGeometry(10, 10, 10); + + for (let i = 0; i < 40; i++) { + const cube1 = new THREE.Mesh(geometry, i % 2 === 0 ? material1 : material2); + cube1.castShadow = true; + cube1.receiveShadow = true; + scene.add(cube1); + cube1.position.set(-i * 25, 20, 30); + cube1.scale.y = Math.random() * 2 + 6; + + const cube2 = new THREE.Mesh(geometry, i % 2 === 0 ? material2 : material1); + cube2.castShadow = true; + cube2.receiveShadow = true; + scene.add(cube2); + cube2.position.set(-i * 25, 20, -30); + cube2.scale.y = Math.random() * 2 + 6; + } + + const gui = new GUI(); + + gui.add(params, 'orthographic').onChange(function (value) { + csm.camera = value ? orthoCamera : camera; + csm.updateFrustums(); + }); + + gui.add(params, 'fade').onChange(function (value) { + csm.fade = value; + csm.updateFrustums(); + }); + + gui.add(params, 'shadows').onChange(function (value) { + renderer.shadowMap.enabled = value; + + scene.traverse(function (child) { + if (child.material) { + child.material.needsUpdate = true; + } + }); + }); + + gui.add(params, 'far', 1, 5000) + .step(1) + .name('shadow far') + .onChange(function (value) { + csm.maxFar = value; + csm.updateFrustums(); + }); + + gui.add(params, 'mode', ['uniform', 'logarithmic', 'practical']) + .name('frustum split mode') + .onChange(function (value) { + csm.mode = value; + csm.updateFrustums(); + }); + + gui.add(params, 'lightX', -1, 1) + .name('light direction x') + .onChange(function (value) { + csm.lightDirection.x = value; + }); + + gui.add(params, 'lightY', -1, 1) + .name('light direction y') + .onChange(function (value) { + csm.lightDirection.y = value; + }); + + gui.add(params, 'lightZ', -1, 1) + .name('light direction z') + .onChange(function (value) { + csm.lightDirection.z = value; + }); + + gui.add(params, 'margin', 0, 200) + .name('light margin') + .onChange(function (value) { + csm.lightMargin = value; + }); + + gui.add(params, 'lightNear', 1, 10000) + .name('light near') + .onChange(function (value) { + for (let i = 0; i < csm.lights.length; i++) { + csm.lights[i].shadow.camera.near = value; + csm.lights[i].shadow.camera.updateProjectionMatrix(); + } + }); + + gui.add(params, 'lightFar', 1, 10000) + .name('light far') + .onChange(function (value) { + for (let i = 0; i < csm.lights.length; i++) { + csm.lights[i].shadow.camera.far = value; + csm.lights[i].shadow.camera.updateProjectionMatrix(); + } + }); + + const helperFolder = gui.addFolder('helper'); + + helperFolder.add(csmHelper, 'visible'); + + helperFolder.add(csmHelper, 'displayFrustum').onChange(function () { + csmHelper.updateVisibility(); + }); + + helperFolder.add(csmHelper, 'displayPlanes').onChange(function () { + csmHelper.updateVisibility(); + }); + + helperFolder.add(csmHelper, 'displayShadowBounds').onChange(function () { + csmHelper.updateVisibility(); + }); + + helperFolder.add(params, 'autoUpdateHelper').name('auto update'); + + helperFolder.add(params, 'updateHelper').name('update'); + + helperFolder.open(); + + window.addEventListener('resize', function () { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + updateOrthoCamera(); + csm.updateFrustums(); + + renderer.setSize(window.innerWidth, window.innerHeight); + }); +} + +function animate() { + camera.updateMatrixWorld(); + csm.update(); + controls.update(); + + if (params.orthographic) { + updateOrthoCamera(); + csm.updateFrustums(); + + if (params.autoUpdateHelper) { + csmHelper.update(); + } + + renderer.render(scene, orthoCamera); + } else { + if (params.autoUpdateHelper) { + csmHelper.update(); + } + + renderer.render(scene, camera); + } +} diff --git a/examples-testing/examples/webgl_shadowmap_pcss.ts b/examples-testing/examples/webgl_shadowmap_pcss.ts new file mode 100644 index 000000000..a47a011ff --- /dev/null +++ b/examples-testing/examples/webgl_shadowmap_pcss.ts @@ -0,0 +1,161 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let stats; +let camera, scene, renderer; + +let group; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + // scene + + scene = new THREE.Scene(); + scene.fog = new THREE.Fog(0xcce0ff, 5, 100); + + // camera + + camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 1, 10000); + + // We use this particular camera position in order to expose a bug that can sometimes happen presumably + // due to lack of precision when interpolating values over really large triangles. + // It reproduced on at least NVIDIA GTX 1080 and GTX 1050 Ti GPUs when the ground plane was not + // subdivided into segments. + camera.position.x = 7; + camera.position.y = 13; + camera.position.z = 7; + + scene.add(camera); + + // lights + + scene.add(new THREE.AmbientLight(0xaaaaaa, 3)); + + const light = new THREE.DirectionalLight(0xf0f6ff, 4.5); + light.position.set(2, 8, 4); + + light.castShadow = true; + light.shadow.mapSize.width = 1024; + light.shadow.mapSize.height = 1024; + light.shadow.camera.far = 20; + + scene.add(light); + + // scene.add( new DirectionalLightHelper( light ) ); + scene.add(new THREE.CameraHelper(light.shadow.camera)); + + // group + + group = new THREE.Group(); + scene.add(group); + + const geometry = new THREE.SphereGeometry(0.3, 20, 20); + + for (let i = 0; i < 20; i++) { + const material = new THREE.MeshPhongMaterial({ color: Math.random() * 0xffffff }); + + const sphere = new THREE.Mesh(geometry, material); + sphere.position.x = Math.random() - 0.5; + sphere.position.z = Math.random() - 0.5; + sphere.position.normalize(); + sphere.position.multiplyScalar(Math.random() * 2 + 1); + sphere.castShadow = true; + sphere.receiveShadow = true; + sphere.userData.phase = Math.random() * Math.PI; + group.add(sphere); + } + + // ground + + const groundMaterial = new THREE.MeshPhongMaterial({ color: 0x898989 }); + + const ground = new THREE.Mesh(new THREE.PlaneGeometry(20000, 20000, 8, 8), groundMaterial); + ground.rotation.x = -Math.PI / 2; + ground.receiveShadow = true; + scene.add(ground); + + // column + + const column = new THREE.Mesh(new THREE.BoxGeometry(1, 4, 1), groundMaterial); + column.position.y = 2; + column.castShadow = true; + column.receiveShadow = true; + scene.add(column); + + // overwrite shadowmap code + + let shader = THREE.ShaderChunk.shadowmap_pars_fragment; + + shader = shader.replace( + '#ifdef USE_SHADOWMAP', + '#ifdef USE_SHADOWMAP' + document.getElementById('PCSS').textContent, + ); + + shader = shader.replace( + '#if defined( SHADOWMAP_TYPE_PCF )', + document.getElementById('PCSSGetShadow').textContent + '#if defined( SHADOWMAP_TYPE_PCF )', + ); + + THREE.ShaderChunk.shadowmap_pars_fragment = shader; + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.setClearColor(scene.fog.color); + + container.appendChild(renderer.domElement); + + renderer.shadowMap.enabled = true; + + // controls + const controls = new OrbitControls(camera, renderer.domElement); + controls.maxPolarAngle = Math.PI * 0.5; + controls.minDistance = 10; + controls.maxDistance = 75; + controls.target.set(0, 2.5, 0); + controls.update(); + + // performance monitor + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +// + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const time = performance.now() / 1000; + + group.traverse(function (child) { + if ('phase' in child.userData) { + child.position.y = Math.abs(Math.sin(time + child.userData.phase)) * 4 + 0.3; + } + }); + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_shadowmap_performance.ts b/examples-testing/examples/webgl_shadowmap_performance.ts new file mode 100644 index 000000000..0e45b63f9 --- /dev/null +++ b/examples-testing/examples/webgl_shadowmap_performance.ts @@ -0,0 +1,281 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { FontLoader } from 'three/addons/loaders/FontLoader.js'; +import { TextGeometry } from 'three/addons/geometries/TextGeometry.js'; + +const SHADOW_MAP_WIDTH = 2048, + SHADOW_MAP_HEIGHT = 1024; + +let SCREEN_WIDTH = window.innerWidth; +let SCREEN_HEIGHT = window.innerHeight; +const FLOOR = -250; + +const ANIMATION_GROUPS = 25; + +let camera, controls, scene, renderer; +let stats; + +const NEAR = 5, + FAR = 3000; + +let morph, mixer; + +const morphs = [], + animGroups = []; + +const clock = new THREE.Clock(); + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + // CAMERA + + camera = new THREE.PerspectiveCamera(23, SCREEN_WIDTH / SCREEN_HEIGHT, NEAR, FAR); + camera.position.set(700, 50, 1900); + + // SCENE + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x59472b); + scene.fog = new THREE.Fog(0x59472b, 1000, FAR); + + // LIGHTS + + const ambient = new THREE.AmbientLight(0xffffff); + scene.add(ambient); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0, 1500, 1000); + light.castShadow = true; + light.shadow.camera.top = 2000; + light.shadow.camera.bottom = -2000; + light.shadow.camera.left = -2000; + light.shadow.camera.right = 2000; + light.shadow.camera.near = 1200; + light.shadow.camera.far = 2500; + light.shadow.bias = 0.0001; + + light.shadow.mapSize.width = SHADOW_MAP_WIDTH; + light.shadow.mapSize.height = SHADOW_MAP_HEIGHT; + + scene.add(light); + + createScene(); + + // RENDERER + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + renderer.autoClear = false; + + // + + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.PCFSoftShadowMap; + + // CONTROLS + + controls = new FirstPersonControls(camera, renderer.domElement); + + controls.lookSpeed = 0.0125; + controls.movementSpeed = 500; + controls.lookVertical = true; + + controls.lookAt(scene.position); + + // STATS + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + SCREEN_WIDTH = window.innerWidth; + SCREEN_HEIGHT = window.innerHeight; + + camera.aspect = SCREEN_WIDTH / SCREEN_HEIGHT; + camera.updateProjectionMatrix(); + + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + + controls.handleResize(); +} + +function createScene() { + // GROUND + + const geometry = new THREE.PlaneGeometry(100, 100); + const planeMaterial = new THREE.MeshPhongMaterial({ color: 0xffdd99 }); + + const ground = new THREE.Mesh(geometry, planeMaterial); + + ground.position.set(0, FLOOR, 0); + ground.rotation.x = -Math.PI / 2; + ground.scale.set(100, 100, 100); + + ground.castShadow = false; + ground.receiveShadow = true; + + scene.add(ground); + + // TEXT + + const loader = new FontLoader(); + loader.load('fonts/helvetiker_bold.typeface.json', function (font) { + const textGeo = new TextGeometry('THREE.JS', { + font: font, + + size: 200, + depth: 50, + curveSegments: 12, + + bevelThickness: 2, + bevelSize: 5, + bevelEnabled: true, + }); + + textGeo.computeBoundingBox(); + const centerOffset = -0.5 * (textGeo.boundingBox.max.x - textGeo.boundingBox.min.x); + + const textMaterial = new THREE.MeshPhongMaterial({ color: 0xff0000, specular: 0xffffff }); + + const mesh = new THREE.Mesh(textGeo, textMaterial); + mesh.position.x = centerOffset; + mesh.position.y = FLOOR + 67; + + mesh.castShadow = true; + mesh.receiveShadow = true; + + scene.add(mesh); + }); + + // CUBES + + const cubes1 = new THREE.Mesh(new THREE.BoxGeometry(1500, 220, 150), planeMaterial); + + cubes1.position.y = FLOOR - 50; + cubes1.position.z = 20; + + cubes1.castShadow = true; + cubes1.receiveShadow = true; + + scene.add(cubes1); + + const cubes2 = new THREE.Mesh(new THREE.BoxGeometry(1600, 170, 250), planeMaterial); + + cubes2.position.y = FLOOR - 50; + cubes2.position.z = 20; + + cubes2.castShadow = true; + cubes2.receiveShadow = true; + + scene.add(cubes2); + + mixer = new THREE.AnimationMixer(scene); + + for (let i = 0; i !== ANIMATION_GROUPS; ++i) { + const group = new THREE.AnimationObjectGroup(); + animGroups.push(group); + } + + // MORPHS + + function addMorph(mesh, clip, speed, duration, x, y, z, fudgeColor, massOptimization) { + mesh = mesh.clone(); + mesh.material = mesh.material.clone(); + + if (fudgeColor) { + mesh.material.color.offsetHSL(0, Math.random() * 0.5 - 0.25, Math.random() * 0.5 - 0.25); + } + + mesh.speed = speed; + + if (massOptimization) { + const index = Math.floor(Math.random() * ANIMATION_GROUPS), + animGroup = animGroups[index]; + + animGroup.add(mesh); + + if (!mixer.existingAction(clip, animGroup)) { + const randomness = 0.6 * Math.random() - 0.3; + const phase = (index + randomness) / ANIMATION_GROUPS; + + mixer + .clipAction(clip, animGroup) + .setDuration(duration) + .startAt(-duration * phase) + .play(); + } + } else { + mixer + .clipAction(clip, mesh) + .setDuration(duration) + .startAt(-duration * Math.random()) + .play(); + } + + mesh.position.set(x, y, z); + mesh.rotation.y = Math.PI / 2; + + mesh.castShadow = true; + mesh.receiveShadow = true; + + scene.add(mesh); + + morphs.push(mesh); + } + + const gltfLoader = new GLTFLoader(); + gltfLoader.load('models/gltf/Horse.glb', function (gltf) { + const mesh = gltf.scene.children[0]; + const clip = gltf.animations[0]; + + for (let i = -600; i < 601; i += 2) { + addMorph(mesh, clip, 550, 1, 100 - Math.random() * 3000, FLOOR, i, true, true); + } + }); +} + +// + +function animate() { + stats.begin(); + render(); + stats.end(); +} + +function render() { + const delta = clock.getDelta(); + + if (mixer) mixer.update(delta); + + for (let i = 0; i < morphs.length; i++) { + morph = morphs[i]; + + morph.position.x += morph.speed * delta; + + if (morph.position.x > 2000) { + morph.position.x = -1000 - Math.random() * 500; + } + } + + controls.update(delta); + + renderer.clear(); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_shadowmap_pointlight.ts b/examples-testing/examples/webgl_shadowmap_pointlight.ts new file mode 100644 index 000000000..c68d69749 --- /dev/null +++ b/examples-testing/examples/webgl_shadowmap_pointlight.ts @@ -0,0 +1,139 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer, stats; +let pointLight, pointLight2; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 10, 40); + + scene = new THREE.Scene(); + scene.add(new THREE.AmbientLight(0x111122, 3)); + + // lights + + function createLight(color) { + const intensity = 200; + + const light = new THREE.PointLight(color, intensity, 20); + light.castShadow = true; + light.shadow.bias = -0.005; // reduces self-shadowing on double-sided objects + + let geometry = new THREE.SphereGeometry(0.3, 12, 6); + let material = new THREE.MeshBasicMaterial({ color: color }); + material.color.multiplyScalar(intensity); + let sphere = new THREE.Mesh(geometry, material); + light.add(sphere); + + const texture = new THREE.CanvasTexture(generateTexture()); + texture.magFilter = THREE.NearestFilter; + texture.wrapT = THREE.RepeatWrapping; + texture.wrapS = THREE.RepeatWrapping; + texture.repeat.set(1, 4.5); + + geometry = new THREE.SphereGeometry(2, 32, 8); + material = new THREE.MeshPhongMaterial({ + side: THREE.DoubleSide, + alphaMap: texture, + alphaTest: 0.5, + }); + + sphere = new THREE.Mesh(geometry, material); + sphere.castShadow = true; + sphere.receiveShadow = true; + light.add(sphere); + + return light; + } + + pointLight = createLight(0x0088ff); + scene.add(pointLight); + + pointLight2 = createLight(0xff8888); + scene.add(pointLight2); + // + + const geometry = new THREE.BoxGeometry(30, 30, 30); + + const material = new THREE.MeshPhongMaterial({ + color: 0xa0adaf, + shininess: 10, + specular: 0x111111, + side: THREE.BackSide, + }); + + const mesh = new THREE.Mesh(geometry, material); + mesh.position.y = 10; + mesh.receiveShadow = true; + scene.add(mesh); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.BasicShadowMap; + document.body.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 10, 0); + controls.update(); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function generateTexture() { + const canvas = document.createElement('canvas'); + canvas.width = 2; + canvas.height = 2; + + const context = canvas.getContext('2d'); + context.fillStyle = 'white'; + context.fillRect(0, 1, 2, 1); + + return canvas; +} + +function animate() { + let time = performance.now() * 0.001; + + pointLight.position.x = Math.sin(time * 0.6) * 9; + pointLight.position.y = Math.sin(time * 0.7) * 9 + 6; + pointLight.position.z = Math.sin(time * 0.8) * 9; + + pointLight.rotation.x = time; + pointLight.rotation.z = time; + + time += 10000; + + pointLight2.position.x = Math.sin(time * 0.6) * 9; + pointLight2.position.y = Math.sin(time * 0.7) * 9 + 6; + pointLight2.position.z = Math.sin(time * 0.8) * 9; + + pointLight2.rotation.x = time; + pointLight2.rotation.z = time; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_shadowmap_progressive.ts b/examples-testing/examples/webgl_shadowmap_progressive.ts new file mode 100644 index 000000000..86ec68172 --- /dev/null +++ b/examples-testing/examples/webgl_shadowmap_progressive.ts @@ -0,0 +1,204 @@ +import * as THREE from 'three'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { TransformControls } from 'three/addons/controls/TransformControls.js'; +import { ProgressiveLightMap } from 'three/addons/misc/ProgressiveLightMap.js'; + +// ShadowMap + LightMap Res and Number of Directional Lights +const shadowMapRes = 512, + lightMapRes = 1024, + lightCount = 8; +let camera, + scene, + renderer, + controls, + control, + control2, + object = new THREE.Mesh(), + lightOrigin = null, + progressiveSurfacemap; +const dirLights = [], + lightmapObjects = []; +const params = { + Enable: true, + 'Blur Edges': true, + 'Blend Window': 200, + 'Light Radius': 50, + 'Ambient Weight': 0.5, + 'Debug Lightmap': false, +}; +init(); +createGUI(); + +function init() { + // renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + document.body.appendChild(renderer.domElement); + + // camera + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 100, 200); + camera.name = 'Camera'; + + // scene + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x949494); + scene.fog = new THREE.Fog(0x949494, 1000, 3000); + + // progressive lightmap + progressiveSurfacemap = new ProgressiveLightMap(renderer, lightMapRes); + + // directional lighting "origin" + lightOrigin = new THREE.Group(); + lightOrigin.position.set(60, 150, 100); + scene.add(lightOrigin); + + // transform gizmo + control = new TransformControls(camera, renderer.domElement); + control.addEventListener('dragging-changed', event => { + controls.enabled = !event.value; + }); + control.attach(lightOrigin); + scene.add(control); + + // create 8 directional lights to speed up the convergence + for (let l = 0; l < lightCount; l++) { + const dirLight = new THREE.DirectionalLight(0xffffff, 1.0 / lightCount); + dirLight.name = 'Dir. Light ' + l; + dirLight.position.set(200, 200, 200); + dirLight.castShadow = true; + dirLight.shadow.camera.near = 100; + dirLight.shadow.camera.far = 5000; + dirLight.shadow.camera.right = 150; + dirLight.shadow.camera.left = -150; + dirLight.shadow.camera.top = 150; + dirLight.shadow.camera.bottom = -150; + dirLight.shadow.mapSize.width = shadowMapRes; + dirLight.shadow.mapSize.height = shadowMapRes; + lightmapObjects.push(dirLight); + dirLights.push(dirLight); + } + + // ground + const groundMesh = new THREE.Mesh( + new THREE.PlaneGeometry(600, 600), + new THREE.MeshPhongMaterial({ color: 0xffffff, depthWrite: true }), + ); + groundMesh.position.y = -0.1; + groundMesh.rotation.x = -Math.PI / 2; + groundMesh.name = 'Ground Mesh'; + lightmapObjects.push(groundMesh); + scene.add(groundMesh); + + // model + function loadModel() { + object.traverse(function (child) { + if (child.isMesh) { + child.name = 'Loaded Mesh'; + child.castShadow = true; + child.receiveShadow = true; + child.material = new THREE.MeshPhongMaterial(); + + // This adds the model to the lightmap + lightmapObjects.push(child); + progressiveSurfacemap.addObjectsToLightMap(lightmapObjects); + } else { + child.layers.disableAll(); // Disable Rendering for this + } + }); + scene.add(object); + object.scale.set(2, 2, 2); + object.position.set(0, -16, 0); + control2 = new TransformControls(camera, renderer.domElement); + control2.addEventListener('dragging-changed', event => { + controls.enabled = !event.value; + }); + control2.attach(object); + scene.add(control2); + const lightTarget = new THREE.Group(); + lightTarget.position.set(0, 20, 0); + for (let l = 0; l < dirLights.length; l++) { + dirLights[l].target = lightTarget; + } + + object.add(lightTarget); + } + + const manager = new THREE.LoadingManager(loadModel); + const loader = new GLTFLoader(manager); + loader.load('models/gltf/ShadowmappableMesh.glb', function (obj) { + object = obj.scene.children[0]; + }); + + // controls + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled + controls.dampingFactor = 0.05; + controls.screenSpacePanning = true; + controls.minDistance = 100; + controls.maxDistance = 500; + controls.maxPolarAngle = Math.PI / 1.5; + controls.target.set(0, 100, 0); + + window.addEventListener('resize', onWindowResize); +} + +function createGUI() { + const gui = new GUI({ title: 'Accumulation Settings' }); + gui.add(params, 'Enable'); + gui.add(params, 'Blur Edges'); + gui.add(params, 'Blend Window', 1, 500).step(1); + gui.add(params, 'Light Radius', 0, 200).step(10); + gui.add(params, 'Ambient Weight', 0, 1).step(0.1); + gui.add(params, 'Debug Lightmap'); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + // Update the inertia on the orbit controls + controls.update(); + + // Accumulate Surface Maps + if (params['Enable']) { + progressiveSurfacemap.update(camera, params['Blend Window'], params['Blur Edges']); + + if (!progressiveSurfacemap.firstUpdate) { + progressiveSurfacemap.showDebugLightmap(params['Debug Lightmap']); + } + } + + // Manually Update the Directional Lights + for (let l = 0; l < dirLights.length; l++) { + // Sometimes they will be sampled from the target direction + // Sometimes they will be uniformly sampled from the upper hemisphere + if (Math.random() > params['Ambient Weight']) { + dirLights[l].position.set( + lightOrigin.position.x + Math.random() * params['Light Radius'], + lightOrigin.position.y + Math.random() * params['Light Radius'], + lightOrigin.position.z + Math.random() * params['Light Radius'], + ); + } else { + // Uniform Hemispherical Surface Distribution for Ambient Occlusion + const lambda = Math.acos(2 * Math.random() - 1) - 3.14159 / 2.0; + const phi = 2 * 3.14159 * Math.random(); + dirLights[l].position.set( + Math.cos(lambda) * Math.cos(phi) * 300 + object.position.x, + Math.abs(Math.cos(lambda) * Math.sin(phi) * 300) + object.position.y + 20, + Math.sin(lambda) * 300 + object.position.z, + ); + } + } + + // Render Scene + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_shadowmap_viewer.ts b/examples-testing/examples/webgl_shadowmap_viewer.ts new file mode 100644 index 000000000..f974ef038 --- /dev/null +++ b/examples-testing/examples/webgl_shadowmap_viewer.ts @@ -0,0 +1,178 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { ShadowMapViewer } from 'three/addons/utils/ShadowMapViewer.js'; + +let camera, scene, renderer, clock, stats; +let dirLight, spotLight; +let torusKnot, cube; +let dirLightShadowMapViewer, spotLightShadowMapViewer; + +init(); + +function init() { + initScene(); + initShadowMapViewers(); + initMisc(); + + document.body.appendChild(renderer.domElement); + window.addEventListener('resize', onWindowResize); +} + +function initScene() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 15, 35); + + scene = new THREE.Scene(); + + // Lights + + scene.add(new THREE.AmbientLight(0x404040, 3)); + + spotLight = new THREE.SpotLight(0xffffff, 500); + spotLight.name = 'Spot Light'; + spotLight.angle = Math.PI / 5; + spotLight.penumbra = 0.3; + spotLight.position.set(10, 10, 5); + spotLight.castShadow = true; + spotLight.shadow.camera.near = 8; + spotLight.shadow.camera.far = 30; + spotLight.shadow.mapSize.width = 1024; + spotLight.shadow.mapSize.height = 1024; + scene.add(spotLight); + + scene.add(new THREE.CameraHelper(spotLight.shadow.camera)); + + dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.name = 'Dir. Light'; + dirLight.position.set(0, 10, 0); + dirLight.castShadow = true; + dirLight.shadow.camera.near = 1; + dirLight.shadow.camera.far = 10; + dirLight.shadow.camera.right = 15; + dirLight.shadow.camera.left = -15; + dirLight.shadow.camera.top = 15; + dirLight.shadow.camera.bottom = -15; + dirLight.shadow.mapSize.width = 1024; + dirLight.shadow.mapSize.height = 1024; + scene.add(dirLight); + + scene.add(new THREE.CameraHelper(dirLight.shadow.camera)); + + // Geometry + let geometry = new THREE.TorusKnotGeometry(25, 8, 75, 20); + let material = new THREE.MeshPhongMaterial({ + color: 0xff0000, + shininess: 150, + specular: 0x222222, + }); + + torusKnot = new THREE.Mesh(geometry, material); + torusKnot.scale.multiplyScalar(1 / 18); + torusKnot.position.y = 3; + torusKnot.castShadow = true; + torusKnot.receiveShadow = true; + scene.add(torusKnot); + + geometry = new THREE.BoxGeometry(3, 3, 3); + cube = new THREE.Mesh(geometry, material); + cube.position.set(8, 3, 8); + cube.castShadow = true; + cube.receiveShadow = true; + scene.add(cube); + + geometry = new THREE.BoxGeometry(10, 0.15, 10); + material = new THREE.MeshPhongMaterial({ + color: 0xa0adaf, + shininess: 150, + specular: 0x111111, + }); + + const ground = new THREE.Mesh(geometry, material); + ground.scale.multiplyScalar(3); + ground.castShadow = false; + ground.receiveShadow = true; + scene.add(ground); +} + +function initShadowMapViewers() { + dirLightShadowMapViewer = new ShadowMapViewer(dirLight); + spotLightShadowMapViewer = new ShadowMapViewer(spotLight); + resizeShadowMapViewers(); +} + +function initMisc() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.BasicShadowMap; + + // Mouse control + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 2, 0); + controls.update(); + + clock = new THREE.Clock(); + + stats = new Stats(); + document.body.appendChild(stats.dom); +} + +function resizeShadowMapViewers() { + const size = window.innerWidth * 0.15; + + dirLightShadowMapViewer.position.x = 10; + dirLightShadowMapViewer.position.y = 10; + dirLightShadowMapViewer.size.width = size; + dirLightShadowMapViewer.size.height = size; + dirLightShadowMapViewer.update(); //Required when setting position or size directly + + spotLightShadowMapViewer.size.set(size, size); + spotLightShadowMapViewer.position.set(size + 20, 10); + // spotLightShadowMapViewer.update(); //NOT required because .set updates automatically +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + resizeShadowMapViewers(); + dirLightShadowMapViewer.updateForWindowResize(); + spotLightShadowMapViewer.updateForWindowResize(); +} + +function animate() { + render(); + + stats.update(); +} + +function renderScene() { + renderer.render(scene, camera); +} + +function renderShadowMapViewers() { + dirLightShadowMapViewer.render(renderer); + spotLightShadowMapViewer.render(renderer); +} + +function render() { + const delta = clock.getDelta(); + + renderScene(); + renderShadowMapViewers(); + + torusKnot.rotation.x += 0.25 * delta; + torusKnot.rotation.y += 2 * delta; + torusKnot.rotation.z += 1 * delta; + + cube.rotation.x += 0.25 * delta; + cube.rotation.y += 2 * delta; + cube.rotation.z += 1 * delta; +} diff --git a/examples-testing/examples/webgl_shadowmap_vsm.ts b/examples-testing/examples/webgl_shadowmap_vsm.ts new file mode 100644 index 000000000..4867c7315 --- /dev/null +++ b/examples-testing/examples/webgl_shadowmap_vsm.ts @@ -0,0 +1,200 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer, clock, stats; +let dirLight, spotLight; +let torusKnot, dirGroup; + +init(); + +function init() { + initScene(); + initMisc(); + + // Init gui + const gui = new GUI(); + + const config = { + spotlightRadius: 4, + spotlightSamples: 8, + dirlightRadius: 4, + dirlightSamples: 8, + }; + + const spotlightFolder = gui.addFolder('Spotlight'); + spotlightFolder + .add(config, 'spotlightRadius') + .name('radius') + .min(0) + .max(25) + .onChange(function (value) { + spotLight.shadow.radius = value; + }); + + spotlightFolder + .add(config, 'spotlightSamples', 1, 25, 1) + .name('samples') + .onChange(function (value) { + spotLight.shadow.blurSamples = value; + }); + spotlightFolder.open(); + + const dirlightFolder = gui.addFolder('Directional Light'); + dirlightFolder + .add(config, 'dirlightRadius') + .name('radius') + .min(0) + .max(25) + .onChange(function (value) { + dirLight.shadow.radius = value; + }); + + dirlightFolder + .add(config, 'dirlightSamples', 1, 25, 1) + .name('samples') + .onChange(function (value) { + dirLight.shadow.blurSamples = value; + }); + dirlightFolder.open(); + + document.body.appendChild(renderer.domElement); + window.addEventListener('resize', onWindowResize); +} + +function initScene() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 10, 30); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x222244); + scene.fog = new THREE.Fog(0x222244, 50, 100); + + // Lights + + scene.add(new THREE.AmbientLight(0x444444)); + + spotLight = new THREE.SpotLight(0xff8888, 400); + spotLight.angle = Math.PI / 5; + spotLight.penumbra = 0.3; + spotLight.position.set(8, 10, 5); + spotLight.castShadow = true; + spotLight.shadow.camera.near = 8; + spotLight.shadow.camera.far = 200; + spotLight.shadow.mapSize.width = 256; + spotLight.shadow.mapSize.height = 256; + spotLight.shadow.bias = -0.002; + spotLight.shadow.radius = 4; + scene.add(spotLight); + + dirLight = new THREE.DirectionalLight(0x8888ff, 3); + dirLight.position.set(3, 12, 17); + dirLight.castShadow = true; + dirLight.shadow.camera.near = 0.1; + dirLight.shadow.camera.far = 500; + dirLight.shadow.camera.right = 17; + dirLight.shadow.camera.left = -17; + dirLight.shadow.camera.top = 17; + dirLight.shadow.camera.bottom = -17; + dirLight.shadow.mapSize.width = 512; + dirLight.shadow.mapSize.height = 512; + dirLight.shadow.radius = 4; + dirLight.shadow.bias = -0.0005; + + dirGroup = new THREE.Group(); + dirGroup.add(dirLight); + scene.add(dirGroup); + + // Geometry + + const geometry = new THREE.TorusKnotGeometry(25, 8, 75, 20); + const material = new THREE.MeshPhongMaterial({ + color: 0x999999, + shininess: 0, + specular: 0x222222, + }); + + torusKnot = new THREE.Mesh(geometry, material); + torusKnot.scale.multiplyScalar(1 / 18); + torusKnot.position.y = 3; + torusKnot.castShadow = true; + torusKnot.receiveShadow = true; + scene.add(torusKnot); + + const cylinderGeometry = new THREE.CylinderGeometry(0.75, 0.75, 7, 32); + + const pillar1 = new THREE.Mesh(cylinderGeometry, material); + pillar1.position.set(8, 3.5, 8); + pillar1.castShadow = true; + pillar1.receiveShadow = true; + + const pillar2 = pillar1.clone(); + pillar2.position.set(8, 3.5, -8); + const pillar3 = pillar1.clone(); + pillar3.position.set(-8, 3.5, 8); + const pillar4 = pillar1.clone(); + pillar4.position.set(-8, 3.5, -8); + + scene.add(pillar1); + scene.add(pillar2); + scene.add(pillar3); + scene.add(pillar4); + + const planeGeometry = new THREE.PlaneGeometry(200, 200); + const planeMaterial = new THREE.MeshPhongMaterial({ + color: 0x999999, + shininess: 0, + specular: 0x111111, + }); + + const ground = new THREE.Mesh(planeGeometry, planeMaterial); + ground.rotation.x = -Math.PI / 2; + ground.scale.multiplyScalar(3); + ground.castShadow = true; + ground.receiveShadow = true; + scene.add(ground); +} + +function initMisc() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.VSMShadowMap; + + // Mouse control + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 2, 0); + controls.update(); + + clock = new THREE.Clock(); + + stats = new Stats(); + document.body.appendChild(stats.dom); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate(time) { + const delta = clock.getDelta(); + + torusKnot.rotation.x += 0.25 * delta; + torusKnot.rotation.y += 0.5 * delta; + torusKnot.rotation.z += 1 * delta; + + dirGroup.rotation.y += 0.7 * delta; + dirLight.position.z = 17 + Math.sin(time * 0.001) * 5; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_shadowmesh.ts b/examples-testing/examples/webgl_shadowmesh.ts new file mode 100644 index 000000000..412fc0283 --- /dev/null +++ b/examples-testing/examples/webgl_shadowmesh.ts @@ -0,0 +1,250 @@ +import * as THREE from 'three'; + +import { ShadowMesh } from 'three/addons/objects/ShadowMesh.js'; + +let SCREEN_WIDTH = window.innerWidth; +let SCREEN_HEIGHT = window.innerHeight; + +const scene = new THREE.Scene(); +const camera = new THREE.PerspectiveCamera(55, SCREEN_WIDTH / SCREEN_HEIGHT, 1, 3000); +const clock = new THREE.Clock(); +const renderer = new THREE.WebGLRenderer({ stencil: true }); + +const sunLight = new THREE.DirectionalLight('rgb(255,255,255)', 3); +let useDirectionalLight = true; +let arrowHelper1, arrowHelper2, arrowHelper3; +const arrowDirection = new THREE.Vector3(); +const arrowPosition1 = new THREE.Vector3(); +const arrowPosition2 = new THREE.Vector3(); +const arrowPosition3 = new THREE.Vector3(); +let groundMesh; +let lightSphere, lightHolder; +let pyramid, pyramidShadow; +let sphere, sphereShadow; +let cube, cubeShadow; +let cylinder, cylinderShadow; +let torus, torusShadow; +const normalVector = new THREE.Vector3(0, 1, 0); +const planeConstant = 0.01; // this value must be slightly higher than the groundMesh's y position of 0.0 +const groundPlane = new THREE.Plane(normalVector, planeConstant); +const lightPosition4D = new THREE.Vector4(); +let verticalAngle = 0; +let horizontalAngle = 0; +let frameTime = 0; +const TWO_PI = Math.PI * 2; + +init(); + +function init() { + scene.background = new THREE.Color(0x0096ff); + + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + renderer.setAnimationLoop(animate); + document.getElementById('container').appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); + + camera.position.set(0, 2.5, 10); + scene.add(camera); + onWindowResize(); + + sunLight.position.set(5, 7, -1); + sunLight.lookAt(scene.position); + scene.add(sunLight); + + lightPosition4D.x = sunLight.position.x; + lightPosition4D.y = sunLight.position.y; + lightPosition4D.z = sunLight.position.z; + // amount of light-ray divergence. Ranging from: + // 0.001 = sunlight(min divergence) to 1.0 = pointlight(max divergence) + lightPosition4D.w = 0.001; // must be slightly greater than 0, due to 0 causing matrixInverse errors + + // YELLOW ARROW HELPERS + arrowDirection.subVectors(scene.position, sunLight.position).normalize(); + + arrowPosition1.copy(sunLight.position); + arrowHelper1 = new THREE.ArrowHelper(arrowDirection, arrowPosition1, 0.9, 0xffff00, 0.25, 0.08); + scene.add(arrowHelper1); + + arrowPosition2.copy(sunLight.position).add(new THREE.Vector3(0, 0.2, 0)); + arrowHelper2 = new THREE.ArrowHelper(arrowDirection, arrowPosition2, 0.9, 0xffff00, 0.25, 0.08); + scene.add(arrowHelper2); + + arrowPosition3.copy(sunLight.position).add(new THREE.Vector3(0, -0.2, 0)); + arrowHelper3 = new THREE.ArrowHelper(arrowDirection, arrowPosition3, 0.9, 0xffff00, 0.25, 0.08); + scene.add(arrowHelper3); + + // LIGHTBULB + const lightSphereGeometry = new THREE.SphereGeometry(0.09); + const lightSphereMaterial = new THREE.MeshBasicMaterial({ color: 'rgb(255,255,255)' }); + lightSphere = new THREE.Mesh(lightSphereGeometry, lightSphereMaterial); + scene.add(lightSphere); + lightSphere.visible = false; + + const lightHolderGeometry = new THREE.CylinderGeometry(0.05, 0.05, 0.13); + const lightHolderMaterial = new THREE.MeshBasicMaterial({ color: 'rgb(75,75,75)' }); + lightHolder = new THREE.Mesh(lightHolderGeometry, lightHolderMaterial); + scene.add(lightHolder); + lightHolder.visible = false; + + // GROUND + const groundGeometry = new THREE.BoxGeometry(30, 0.01, 40); + const groundMaterial = new THREE.MeshLambertMaterial({ color: 'rgb(0,130,0)' }); + groundMesh = new THREE.Mesh(groundGeometry, groundMaterial); + groundMesh.position.y = 0.0; //this value must be slightly lower than the planeConstant (0.01) parameter above + scene.add(groundMesh); + + // RED CUBE and CUBE's SHADOW + const cubeGeometry = new THREE.BoxGeometry(1, 1, 1); + const cubeMaterial = new THREE.MeshLambertMaterial({ color: 'rgb(255,0,0)', emissive: 0x200000 }); + cube = new THREE.Mesh(cubeGeometry, cubeMaterial); + cube.position.z = -1; + scene.add(cube); + + cubeShadow = new ShadowMesh(cube); + scene.add(cubeShadow); + + // BLUE CYLINDER and CYLINDER's SHADOW + const cylinderGeometry = new THREE.CylinderGeometry(0.3, 0.3, 2); + const cylinderMaterial = new THREE.MeshPhongMaterial({ color: 'rgb(0,0,255)', emissive: 0x000020 }); + cylinder = new THREE.Mesh(cylinderGeometry, cylinderMaterial); + cylinder.position.z = -2.5; + scene.add(cylinder); + + cylinderShadow = new ShadowMesh(cylinder); + scene.add(cylinderShadow); + + // MAGENTA TORUS and TORUS' SHADOW + const torusGeometry = new THREE.TorusGeometry(1, 0.2, 10, 16, TWO_PI); + const torusMaterial = new THREE.MeshPhongMaterial({ color: 'rgb(255,0,255)', emissive: 0x200020 }); + torus = new THREE.Mesh(torusGeometry, torusMaterial); + torus.position.z = -6; + scene.add(torus); + + torusShadow = new ShadowMesh(torus); + scene.add(torusShadow); + + // WHITE SPHERE and SPHERE'S SHADOW + const sphereGeometry = new THREE.SphereGeometry(0.5, 20, 10); + const sphereMaterial = new THREE.MeshPhongMaterial({ color: 'rgb(255,255,255)', emissive: 0x222222 }); + sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); + sphere.position.set(4, 0.5, 2); + scene.add(sphere); + + sphereShadow = new ShadowMesh(sphere); + scene.add(sphereShadow); + + // YELLOW PYRAMID and PYRAMID'S SHADOW + const pyramidGeometry = new THREE.CylinderGeometry(0, 0.5, 2, 4); + const pyramidMaterial = new THREE.MeshPhongMaterial({ + color: 'rgb(255,255,0)', + emissive: 0x440000, + flatShading: true, + shininess: 0, + }); + pyramid = new THREE.Mesh(pyramidGeometry, pyramidMaterial); + pyramid.position.set(-4, 1, 2); + scene.add(pyramid); + + pyramidShadow = new ShadowMesh(pyramid); + scene.add(pyramidShadow); + + document.getElementById('lightButton').addEventListener('click', lightButtonHandler); +} + +function animate() { + frameTime = clock.getDelta(); + + cube.rotation.x += 1.0 * frameTime; + cube.rotation.y += 1.0 * frameTime; + + cylinder.rotation.y += 1.0 * frameTime; + cylinder.rotation.z -= 1.0 * frameTime; + + torus.rotation.x -= 1.0 * frameTime; + torus.rotation.y -= 1.0 * frameTime; + + pyramid.rotation.y += 0.5 * frameTime; + + horizontalAngle += 0.5 * frameTime; + if (horizontalAngle > TWO_PI) horizontalAngle -= TWO_PI; + cube.position.x = Math.sin(horizontalAngle) * 4; + cylinder.position.x = Math.sin(horizontalAngle) * -4; + torus.position.x = Math.cos(horizontalAngle) * 4; + + verticalAngle += 1.5 * frameTime; + if (verticalAngle > TWO_PI) verticalAngle -= TWO_PI; + cube.position.y = Math.sin(verticalAngle) * 2 + 2.9; + cylinder.position.y = Math.sin(verticalAngle) * 2 + 3.1; + torus.position.y = Math.cos(verticalAngle) * 2 + 3.3; + + // update the ShadowMeshes to follow their shadow-casting objects + cubeShadow.update(groundPlane, lightPosition4D); + cylinderShadow.update(groundPlane, lightPosition4D); + torusShadow.update(groundPlane, lightPosition4D); + sphereShadow.update(groundPlane, lightPosition4D); + pyramidShadow.update(groundPlane, lightPosition4D); + + renderer.render(scene, camera); +} + +function onWindowResize() { + SCREEN_WIDTH = window.innerWidth; + SCREEN_HEIGHT = window.innerHeight; + + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + + camera.aspect = SCREEN_WIDTH / SCREEN_HEIGHT; + camera.updateProjectionMatrix(); +} + +function lightButtonHandler() { + useDirectionalLight = !useDirectionalLight; + + if (useDirectionalLight) { + scene.background.setHex(0x0096ff); + + groundMesh.material.color.setHex(0x008200); + sunLight.position.set(5, 7, -1); + sunLight.lookAt(scene.position); + + lightPosition4D.x = sunLight.position.x; + lightPosition4D.y = sunLight.position.y; + lightPosition4D.z = sunLight.position.z; + lightPosition4D.w = 0.001; // more of a directional Light value + + arrowHelper1.visible = true; + arrowHelper2.visible = true; + arrowHelper3.visible = true; + + lightSphere.visible = false; + lightHolder.visible = false; + + document.getElementById('lightButton').value = 'Switch to PointLight'; + } else { + scene.background.setHex(0x000000); + + groundMesh.material.color.setHex(0x969696); + + sunLight.position.set(0, 6, -2); + sunLight.lookAt(scene.position); + lightSphere.position.copy(sunLight.position); + lightHolder.position.copy(lightSphere.position); + lightHolder.position.y += 0.12; + + lightPosition4D.x = sunLight.position.x; + lightPosition4D.y = sunLight.position.y; + lightPosition4D.z = sunLight.position.z; + lightPosition4D.w = 0.9; // more of a point Light value + + arrowHelper1.visible = false; + arrowHelper2.visible = false; + arrowHelper3.visible = false; + + lightSphere.visible = true; + lightHolder.visible = true; + + document.getElementById('lightButton').value = 'Switch to THREE.DirectionalLight'; + } +} diff --git a/examples-testing/examples/webgl_simple_gi.ts b/examples-testing/examples/webgl_simple_gi.ts new file mode 100644 index 000000000..4ab6dc895 --- /dev/null +++ b/examples-testing/examples/webgl_simple_gi.ts @@ -0,0 +1,172 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +class GIMesh extends THREE.Mesh { + copy(source) { + super.copy(source); + + this.geometry = source.geometry.clone(); + + return this; + } +} + +// + +const SimpleGI = function (renderer, scene) { + const SIZE = 32, + SIZE2 = SIZE * SIZE; + + const camera = new THREE.PerspectiveCamera(90, 1, 0.01, 100); + + scene.updateMatrixWorld(true); + + let clone = scene.clone(); + clone.matrixWorldAutoUpdate = false; + + const rt = new THREE.WebGLRenderTarget(SIZE, SIZE); + + const normalMatrix = new THREE.Matrix3(); + + const position = new THREE.Vector3(); + const normal = new THREE.Vector3(); + + let bounces = 0; + let currentVertex = 0; + + const color = new Float32Array(3); + const buffer = new Uint8Array(SIZE2 * 4); + + function compute() { + if (bounces === 3) return; + + const object = scene.children[0]; // torusKnot + const geometry = object.geometry; + + const attributes = geometry.attributes; + const positions = attributes.position.array; + const normals = attributes.normal.array; + + if (attributes.color === undefined) { + const colors = new Float32Array(positions.length); + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3).setUsage(THREE.DynamicDrawUsage)); + } + + const colors = attributes.color.array; + + const startVertex = currentVertex; + const totalVertex = positions.length / 3; + + for (let i = 0; i < 32; i++) { + if (currentVertex >= totalVertex) break; + + position.fromArray(positions, currentVertex * 3); + position.applyMatrix4(object.matrixWorld); + + normal.fromArray(normals, currentVertex * 3); + normal.applyMatrix3(normalMatrix.getNormalMatrix(object.matrixWorld)).normalize(); + + camera.position.copy(position); + camera.lookAt(position.add(normal)); + + renderer.setRenderTarget(rt); + renderer.render(clone, camera); + + renderer.readRenderTargetPixels(rt, 0, 0, SIZE, SIZE, buffer); + + color[0] = 0; + color[1] = 0; + color[2] = 0; + + for (let k = 0, kl = buffer.length; k < kl; k += 4) { + color[0] += buffer[k + 0]; + color[1] += buffer[k + 1]; + color[2] += buffer[k + 2]; + } + + colors[currentVertex * 3 + 0] = color[0] / (SIZE2 * 255); + colors[currentVertex * 3 + 1] = color[1] / (SIZE2 * 255); + colors[currentVertex * 3 + 2] = color[2] / (SIZE2 * 255); + + currentVertex++; + } + + attributes.color.addUpdateRange(startVertex * 3, (currentVertex - startVertex) * 3); + attributes.color.needsUpdate = true; + + if (currentVertex >= totalVertex) { + clone = scene.clone(); + clone.matrixWorldAutoUpdate = false; + + bounces++; + currentVertex = 0; + } + + requestAnimationFrame(compute); + } + + requestAnimationFrame(compute); +}; + +// + +let camera, scene, renderer; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.z = 4; + + scene = new THREE.Scene(); + + // torus knot + + const torusGeometry = new THREE.TorusKnotGeometry(0.75, 0.3, 128, 32, 1); + const material = new THREE.MeshBasicMaterial({ vertexColors: true }); + + const torusKnot = new GIMesh(torusGeometry, material); + scene.add(torusKnot); + + // room + + const materials = []; + + for (let i = 0; i < 8; i++) { + materials.push(new THREE.MeshBasicMaterial({ color: Math.random() * 0xffffff, side: THREE.BackSide })); + } + + const boxGeometry = new THREE.BoxGeometry(3, 3, 3); + + const box = new THREE.Mesh(boxGeometry, materials); + scene.add(box); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + new SimpleGI(renderer, scene); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 1; + controls.maxDistance = 10; + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.setRenderTarget(null); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_sprites.ts b/examples-testing/examples/webgl_sprites.ts new file mode 100644 index 000000000..2e4189347 --- /dev/null +++ b/examples-testing/examples/webgl_sprites.ts @@ -0,0 +1,187 @@ +import * as THREE from 'three'; + +let camera, scene, renderer; +let cameraOrtho, sceneOrtho; + +let spriteTL, spriteTR, spriteBL, spriteBR, spriteC; + +let mapC; + +let group; + +init(); + +function init() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera = new THREE.PerspectiveCamera(60, width / height, 1, 2100); + camera.position.z = 1500; + + cameraOrtho = new THREE.OrthographicCamera(-width / 2, width / 2, height / 2, -height / 2, 1, 10); + cameraOrtho.position.z = 10; + + scene = new THREE.Scene(); + scene.fog = new THREE.Fog(0x000000, 1500, 2100); + + sceneOrtho = new THREE.Scene(); + + // create sprites + + const amount = 200; + const radius = 500; + + const textureLoader = new THREE.TextureLoader(); + + textureLoader.load('textures/sprite0.png', createHUDSprites); + const mapB = textureLoader.load('textures/sprite1.png'); + mapC = textureLoader.load('textures/sprite2.png'); + + mapB.colorSpace = THREE.SRGBColorSpace; + mapC.colorSpace = THREE.SRGBColorSpace; + + group = new THREE.Group(); + + const materialC = new THREE.SpriteMaterial({ map: mapC, color: 0xffffff, fog: true }); + const materialB = new THREE.SpriteMaterial({ map: mapB, color: 0xffffff, fog: true }); + + for (let a = 0; a < amount; a++) { + const x = Math.random() - 0.5; + const y = Math.random() - 0.5; + const z = Math.random() - 0.5; + + let material; + + if (z < 0) { + material = materialB.clone(); + } else { + material = materialC.clone(); + material.color.setHSL(0.5 * Math.random(), 0.75, 0.5); + material.map.offset.set(-0.5, -0.5); + material.map.repeat.set(2, 2); + } + + const sprite = new THREE.Sprite(material); + + sprite.position.set(x, y, z); + sprite.position.normalize(); + sprite.position.multiplyScalar(radius); + + group.add(sprite); + } + + scene.add(group); + + // renderer + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.autoClear = false; // To allow render overlay on top of sprited sphere + + document.body.appendChild(renderer.domElement); + + // + + window.addEventListener('resize', onWindowResize); +} + +function createHUDSprites(texture) { + texture.colorSpace = THREE.SRGBColorSpace; + + const material = new THREE.SpriteMaterial({ map: texture }); + + const width = material.map.image.width; + const height = material.map.image.height; + + spriteTL = new THREE.Sprite(material); + spriteTL.center.set(0.0, 1.0); + spriteTL.scale.set(width, height, 1); + sceneOrtho.add(spriteTL); + + spriteTR = new THREE.Sprite(material); + spriteTR.center.set(1.0, 1.0); + spriteTR.scale.set(width, height, 1); + sceneOrtho.add(spriteTR); + + spriteBL = new THREE.Sprite(material); + spriteBL.center.set(0.0, 0.0); + spriteBL.scale.set(width, height, 1); + sceneOrtho.add(spriteBL); + + spriteBR = new THREE.Sprite(material); + spriteBR.center.set(1.0, 0.0); + spriteBR.scale.set(width, height, 1); + sceneOrtho.add(spriteBR); + + spriteC = new THREE.Sprite(material); + spriteC.center.set(0.5, 0.5); + spriteC.scale.set(width, height, 1); + sceneOrtho.add(spriteC); + + updateHUDSprites(); +} + +function updateHUDSprites() { + const width = window.innerWidth / 2; + const height = window.innerHeight / 2; + + spriteTL.position.set(-width, height, 1); // top left + spriteTR.position.set(width, height, 1); // top right + spriteBL.position.set(-width, -height, 1); // bottom left + spriteBR.position.set(width, -height, 1); // bottom right + spriteC.position.set(0, 0, 1); // center +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + cameraOrtho.left = -width / 2; + cameraOrtho.right = width / 2; + cameraOrtho.top = height / 2; + cameraOrtho.bottom = -height / 2; + cameraOrtho.updateProjectionMatrix(); + + updateHUDSprites(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const time = Date.now() / 1000; + + for (let i = 0, l = group.children.length; i < l; i++) { + const sprite = group.children[i]; + const material = sprite.material; + const scale = Math.sin(time + sprite.position.x * 0.01) * 0.3 + 1.0; + + let imageWidth = 1; + let imageHeight = 1; + + if (material.map && material.map.image && material.map.image.width) { + imageWidth = material.map.image.width; + imageHeight = material.map.image.height; + } + + sprite.material.rotation += 0.1 * (i / l); + sprite.scale.set(scale * imageWidth, scale * imageHeight, 1.0); + + if (material.map !== mapC) { + material.opacity = Math.sin(time + sprite.position.x * 0.01) * 0.4 + 0.6; + } + } + + group.rotation.x = time * 0.5; + group.rotation.y = time * 0.75; + group.rotation.z = time * 1.0; + + renderer.clear(); + renderer.render(scene, camera); + renderer.clearDepth(); + renderer.render(sceneOrtho, cameraOrtho); +} diff --git a/examples-testing/examples/webgl_test_memory.ts b/examples-testing/examples/webgl_test_memory.ts new file mode 100644 index 000000000..f5d0e112d --- /dev/null +++ b/examples-testing/examples/webgl_test_memory.ts @@ -0,0 +1,65 @@ +import * as THREE from 'three'; + +let camera, scene, renderer; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 200; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); +} + +function createImage() { + const canvas = document.createElement('canvas'); + canvas.width = 256; + canvas.height = 256; + + const context = canvas.getContext('2d'); + context.fillStyle = + 'rgb(' + + Math.floor(Math.random() * 256) + + ',' + + Math.floor(Math.random() * 256) + + ',' + + Math.floor(Math.random() * 256) + + ')'; + context.fillRect(0, 0, 256, 256); + + return canvas; +} + +// + +function animate() { + const geometry = new THREE.SphereGeometry(50, Math.random() * 64, Math.random() * 32); + + const texture = new THREE.CanvasTexture(createImage()); + + const material = new THREE.MeshBasicMaterial({ map: texture, wireframe: true }); + + const mesh = new THREE.Mesh(geometry, material); + + scene.add(mesh); + + renderer.render(scene, camera); + + scene.remove(mesh); + + // clean up + + geometry.dispose(); + material.dispose(); + texture.dispose(); +} diff --git a/examples-testing/examples/webgl_test_memory2.ts b/examples-testing/examples/webgl_test_memory2.ts new file mode 100644 index 000000000..366a27914 --- /dev/null +++ b/examples-testing/examples/webgl_test_memory2.ts @@ -0,0 +1,81 @@ +import * as THREE from 'three'; + +const N = 100; + +let container; + +let camera, scene, renderer; + +let geometry; + +const meshes = []; + +let fragmentShader, vertexShader; + +init(); +setInterval(render, 1000 / 60); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + vertexShader = document.getElementById('vertexShader').textContent; + fragmentShader = document.getElementById('fragmentShader').textContent; + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 2000; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + + geometry = new THREE.SphereGeometry(15, 64, 32); + + for (let i = 0; i < N; i++) { + const material = new THREE.ShaderMaterial({ + vertexShader: vertexShader, + fragmentShader: generateFragmentShader(), + }); + + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.x = (0.5 - Math.random()) * 1000; + mesh.position.y = (0.5 - Math.random()) * 1000; + mesh.position.z = (0.5 - Math.random()) * 1000; + + scene.add(mesh); + + meshes.push(mesh); + } + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + container.appendChild(renderer.domElement); +} + +// + +function generateFragmentShader() { + return fragmentShader.replace('XXX', Math.random() + ',' + Math.random() + ',' + Math.random()); +} + +function render() { + for (let i = 0; i < N; i++) { + const mesh = meshes[i]; + mesh.material = new THREE.ShaderMaterial({ + vertexShader: vertexShader, + fragmentShader: generateFragmentShader(), + }); + } + + renderer.render(scene, camera); + + console.log('before', renderer.info.programs.length); + + for (let i = 0; i < N; i++) { + const mesh = meshes[i]; + mesh.material.dispose(); + } + + console.log('after', renderer.info.programs.length); +} diff --git a/examples-testing/examples/webgl_test_wide_gamut.ts b/examples-testing/examples/webgl_test_wide_gamut.ts new file mode 100644 index 000000000..693dd4593 --- /dev/null +++ b/examples-testing/examples/webgl_test_wide_gamut.ts @@ -0,0 +1,118 @@ +import * as THREE from 'three'; + +import WebGL from 'three/addons/capabilities/WebGL.js'; + +let container, camera, renderer, loader; +let sceneL, sceneR, textureL, textureR; + +let sliderPos = window.innerWidth / 2; + +const slider = document.querySelector('.slider'); + +const isP3Context = WebGL.isColorSpaceAvailable(THREE.DisplayP3ColorSpace); + +if (isP3Context) { + THREE.ColorManagement.workingColorSpace = THREE.LinearDisplayP3ColorSpace; +} + +init(); + +function init() { + container = document.querySelector('.container'); + + sceneL = new THREE.Scene(); + sceneR = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.z = 6; + + loader = new THREE.TextureLoader(); + + initTextures(); + initSlider(); + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.setScissorTest(true); + container.appendChild(renderer.domElement); + + if (isP3Context && window.matchMedia('( color-gamut: p3 )').matches) { + renderer.outputColorSpace = THREE.DisplayP3ColorSpace; + } + + window.addEventListener('resize', onWindowResize); + window.matchMedia('( color-gamut: p3 )').addEventListener('change', onGamutChange); +} + +async function initTextures() { + const path = 'textures/wide_gamut/logo_{colorSpace}.png'; + + textureL = await loader.loadAsync(path.replace('{colorSpace}', 'srgb')); + textureR = await loader.loadAsync(path.replace('{colorSpace}', 'p3')); + + textureL.colorSpace = THREE.SRGBColorSpace; + textureR.colorSpace = THREE.DisplayP3ColorSpace; + + sceneL.background = THREE.TextureUtils.contain(textureL, window.innerWidth / window.innerHeight); + sceneR.background = THREE.TextureUtils.contain(textureR, window.innerWidth / window.innerHeight); +} + +function initSlider() { + function onPointerDown() { + if (event.isPrimary === false) return; + + window.addEventListener('pointermove', onPointerMove); + window.addEventListener('pointerup', onPointerUp); + } + + function onPointerUp() { + window.removeEventListener('pointermove', onPointerMove); + window.removeEventListener('pointerup', onPointerUp); + } + + function onPointerMove(e) { + if (event.isPrimary === false) return; + + updateSlider(e.pageX); + } + + updateSlider(sliderPos); + + slider.style.touchAction = 'none'; // disable touch scroll + slider.addEventListener('pointerdown', onPointerDown); +} + +function updateSlider(offset) { + sliderPos = Math.max(10, Math.min(window.innerWidth - 10, offset)); + + slider.style.left = sliderPos - slider.offsetWidth / 2 + 'px'; +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + THREE.TextureUtils.contain(sceneL.background, window.innerWidth / window.innerHeight); + THREE.TextureUtils.contain(sceneR.background, window.innerWidth / window.innerHeight); + + updateSlider(sliderPos); +} + +function onGamutChange({ matches }) { + renderer.outputColorSpace = isP3Context && matches ? THREE.DisplayP3ColorSpace : THREE.SRGBColorSpace; + + textureL.needsUpdate = true; + textureR.needsUpdate = true; +} + +function animate() { + renderer.setScissor(0, 0, sliderPos, window.innerHeight); + renderer.render(sceneL, camera); + + renderer.setScissor(sliderPos, 0, window.innerWidth, window.innerHeight); + renderer.render(sceneR, camera); +} diff --git a/examples-testing/examples/webgl_texture2darray_compressed.ts b/examples-testing/examples/webgl_texture2darray_compressed.ts new file mode 100644 index 000000000..f263be706 --- /dev/null +++ b/examples-testing/examples/webgl_texture2darray_compressed.ts @@ -0,0 +1,88 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'; + +let camera, scene, mesh, renderer, stats, clock; + +const planeWidth = 50; +const planeHeight = 25; + +let depthStep = 1; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 2000); + camera.position.z = 70; + + scene = new THREE.Scene(); + + // + clock = new THREE.Clock(); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + const ktx2Loader = new KTX2Loader(); + ktx2Loader.setTranscoderPath('jsm/libs/basis/'); + ktx2Loader.detectSupport(renderer); + + ktx2Loader.load('textures/spiritedaway.ktx2', function (texturearray) { + const material = new THREE.ShaderMaterial({ + uniforms: { + diffuse: { value: texturearray }, + depth: { value: 55 }, + size: { value: new THREE.Vector2(planeWidth, planeHeight) }, + }, + vertexShader: document.getElementById('vs').textContent.trim(), + fragmentShader: document.getElementById('fs').textContent.trim(), + glslVersion: THREE.GLSL3, + }); + + const geometry = new THREE.PlaneGeometry(planeWidth, planeHeight); + + mesh = new THREE.Mesh(geometry, material); + + scene.add(mesh); + }); + + stats = new Stats(); + container.appendChild(stats.dom); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + if (mesh) { + const delta = clock.getDelta() * 10; + + depthStep += delta; + + const value = depthStep % 5; + + mesh.material.uniforms['depth'].value = value; + } + + render(); + stats.update(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_texture2darray_layerupdate.ts b/examples-testing/examples/webgl_texture2darray_layerupdate.ts new file mode 100644 index 000000000..0cc136cb7 --- /dev/null +++ b/examples-testing/examples/webgl_texture2darray_layerupdate.ts @@ -0,0 +1,131 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'; + +let camera, scene, mesh, renderer; + +const planeWidth = 20; +const planeHeight = 10; + +init(); + +async function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 2000); + camera.position.z = 70; + + scene = new THREE.Scene(); + + // Configure the renderer. + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + container.appendChild(renderer.domElement); + + // Configure the KTX2 loader. + + const ktx2Loader = new KTX2Loader(); + ktx2Loader.setTranscoderPath('jsm/libs/basis/'); + ktx2Loader.detectSupport(renderer); + + // Load several KTX2 textures which will later be used to modify + // specific texture array layers. + + const spiritedaway = await ktx2Loader.loadAsync('textures/spiritedaway.ktx2'); + + // Create a texture array for rendering. + + const layerByteLength = THREE.TextureUtils.getByteLength( + spiritedaway.image.width, + spiritedaway.image.height, + spiritedaway.format, + spiritedaway.type, + ); + + const textureArray = new THREE.CompressedArrayTexture( + [ + { + data: new Uint8Array(layerByteLength * 3), + width: spiritedaway.image.width, + height: spiritedaway.image.height, + }, + ], + spiritedaway.image.width, + spiritedaway.image.height, + 3, + spiritedaway.format, + spiritedaway.type, + ); + + // Setup the GUI + + const formData = { + srcLayer: 0, + destLayer: 0, + transfer() { + const layerElementLength = layerByteLength / spiritedaway.mipmaps[0].data.BYTES_PER_ELEMENT; + textureArray.mipmaps[0].data.set( + spiritedaway.mipmaps[0].data.subarray( + layerElementLength * (formData.srcLayer % spiritedaway.image.depth), + layerElementLength * ((formData.srcLayer % spiritedaway.image.depth) + 1), + ), + layerByteLength * formData.destLayer, + ); + textureArray.addLayerUpdate(formData.destLayer); + textureArray.needsUpdate = true; + + renderer.render(scene, camera); + }, + }; + + const gui = new GUI(); + gui.add(formData, 'srcLayer', 0, spiritedaway.image.depth - 1, 1); + gui.add(formData, 'destLayer', 0, textureArray.image.depth - 1, 1); + gui.add(formData, 'transfer'); + + /// Setup the scene. + + const material = new THREE.ShaderMaterial({ + uniforms: { + diffuse: { value: textureArray }, + size: { value: new THREE.Vector2(planeWidth, planeHeight) }, + }, + vertexShader: document.getElementById('vs').textContent.trim(), + fragmentShader: document.getElementById('fs').textContent.trim(), + glslVersion: THREE.GLSL3, + }); + + const geometry = new THREE.InstancedBufferGeometry(); + geometry.copy(new THREE.PlaneGeometry(planeWidth, planeHeight)); + geometry.instanceCount = 3; + + const instancedIndexAttribute = new THREE.InstancedBufferAttribute(new Uint16Array([0, 1, 2]), 1, false, 1); + instancedIndexAttribute.gpuType = THREE.IntType; + geometry.setAttribute('instancedIndex', instancedIndexAttribute); + + mesh = new THREE.InstancedMesh(geometry, material, 3); + + scene.add(mesh); + + window.addEventListener('resize', onWindowResize); + + // Initialize the texture array by first rendering the spirited away + // frames in order. + + textureArray.mipmaps[0].data.set(spiritedaway.mipmaps[0].data.subarray(0, textureArray.mipmaps[0].data.length)); + textureArray.needsUpdate = true; + renderer.render(scene, camera); +} + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_texture3d.ts b/examples-testing/examples/webgl_texture3d.ts new file mode 100644 index 000000000..977dbadb7 --- /dev/null +++ b/examples-testing/examples/webgl_texture3d.ts @@ -0,0 +1,128 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { NRRDLoader } from 'three/addons/loaders/NRRDLoader.js'; +import { VolumeRenderShader1 } from 'three/addons/shaders/VolumeShader.js'; + +let renderer, scene, camera, controls, material, volconfig, cmtextures; + +init(); + +function init() { + scene = new THREE.Scene(); + + // Create renderer + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + // Create camera (The volume renderer does not work very well with perspective yet) + const h = 512; // frustum height + const aspect = window.innerWidth / window.innerHeight; + camera = new THREE.OrthographicCamera((-h * aspect) / 2, (h * aspect) / 2, h / 2, -h / 2, 1, 1000); + camera.position.set(-64, -64, 128); + camera.up.set(0, 0, 1); // In our data, z is up + + // Create controls + controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); + controls.target.set(64, 64, 128); + controls.minZoom = 0.5; + controls.maxZoom = 4; + controls.enablePan = false; + controls.update(); + + // scene.add( new AxesHelper( 128 ) ); + + // Lighting is baked into the shader a.t.m. + // let dirLight = new DirectionalLight( 0xffffff ); + + // The gui for interaction + volconfig = { clim1: 0, clim2: 1, renderstyle: 'iso', isothreshold: 0.15, colormap: 'viridis' }; + const gui = new GUI(); + gui.add(volconfig, 'clim1', 0, 1, 0.01).onChange(updateUniforms); + gui.add(volconfig, 'clim2', 0, 1, 0.01).onChange(updateUniforms); + gui.add(volconfig, 'colormap', { gray: 'gray', viridis: 'viridis' }).onChange(updateUniforms); + gui.add(volconfig, 'renderstyle', { mip: 'mip', iso: 'iso' }).onChange(updateUniforms); + gui.add(volconfig, 'isothreshold', 0, 1, 0.01).onChange(updateUniforms); + + // Load the data ... + new NRRDLoader().load('models/nrrd/stent.nrrd', function (volume) { + // Texture to hold the volume. We have scalars, so we put our data in the red channel. + // THREEJS will select R32F (33326) based on the THREE.RedFormat and THREE.FloatType. + // Also see https://www.khronos.org/registry/webgl/specs/latest/2.0/#TEXTURE_TYPES_FORMATS_FROM_DOM_ELEMENTS_TABLE + // TODO: look the dtype up in the volume metadata + const texture = new THREE.Data3DTexture(volume.data, volume.xLength, volume.yLength, volume.zLength); + texture.format = THREE.RedFormat; + texture.type = THREE.FloatType; + texture.minFilter = texture.magFilter = THREE.LinearFilter; + texture.unpackAlignment = 1; + texture.needsUpdate = true; + + // Colormap textures + cmtextures = { + viridis: new THREE.TextureLoader().load('textures/cm_viridis.png', render), + gray: new THREE.TextureLoader().load('textures/cm_gray.png', render), + }; + + // Material + const shader = VolumeRenderShader1; + + const uniforms = THREE.UniformsUtils.clone(shader.uniforms); + + uniforms['u_data'].value = texture; + uniforms['u_size'].value.set(volume.xLength, volume.yLength, volume.zLength); + uniforms['u_clim'].value.set(volconfig.clim1, volconfig.clim2); + uniforms['u_renderstyle'].value = volconfig.renderstyle == 'mip' ? 0 : 1; // 0: MIP, 1: ISO + uniforms['u_renderthreshold'].value = volconfig.isothreshold; // For ISO renderstyle + uniforms['u_cmdata'].value = cmtextures[volconfig.colormap]; + + material = new THREE.ShaderMaterial({ + uniforms: uniforms, + vertexShader: shader.vertexShader, + fragmentShader: shader.fragmentShader, + side: THREE.BackSide, // The volume shader uses the backface as its "reference point" + }); + + // THREE.Mesh + const geometry = new THREE.BoxGeometry(volume.xLength, volume.yLength, volume.zLength); + geometry.translate(volume.xLength / 2 - 0.5, volume.yLength / 2 - 0.5, volume.zLength / 2 - 0.5); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + render(); + }); + + window.addEventListener('resize', onWindowResize); +} + +function updateUniforms() { + material.uniforms['u_clim'].value.set(volconfig.clim1, volconfig.clim2); + material.uniforms['u_renderstyle'].value = volconfig.renderstyle == 'mip' ? 0 : 1; // 0: MIP, 1: ISO + material.uniforms['u_renderthreshold'].value = volconfig.isothreshold; // For ISO renderstyle + material.uniforms['u_cmdata'].value = cmtextures[volconfig.colormap]; + + render(); +} + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); + + const aspect = window.innerWidth / window.innerHeight; + + const frustumHeight = camera.top - camera.bottom; + + camera.left = (-frustumHeight * aspect) / 2; + camera.right = (frustumHeight * aspect) / 2; + + camera.updateProjectionMatrix(); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_texture3d_partialupdate.ts b/examples-testing/examples/webgl_texture3d_partialupdate.ts new file mode 100644 index 000000000..1ad6d2646 --- /dev/null +++ b/examples-testing/examples/webgl_texture3d_partialupdate.ts @@ -0,0 +1,326 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +const INITIAL_CLOUD_SIZE = 128; + +let renderer, scene, camera; +let mesh; +let prevTime = performance.now(); +let cloudTexture = null; + +init(); + +function generateCloudTexture(size, scaleFactor = 1.0) { + const data = new Uint8Array(size * size * size); + const scale = (scaleFactor * 10.0) / size; + + let i = 0; + const perlin = new ImprovedNoise(); + const vector = new THREE.Vector3(); + + for (let z = 0; z < size; z++) { + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + const dist = vector + .set(x, y, z) + .subScalar(size / 2) + .divideScalar(size) + .length(); + const fadingFactor = (1.0 - dist) * (1.0 - dist); + data[i] = (128 + 128 * perlin.noise((x * scale) / 1.5, y * scale, (z * scale) / 1.5)) * fadingFactor; + + i++; + } + } + } + + return new THREE.Data3DTexture(data, size, size, size); +} + +function init() { + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0, 0, 1.5); + + new OrbitControls(camera, renderer.domElement); + + // Sky + + const canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 32; + + const context = canvas.getContext('2d'); + const gradient = context.createLinearGradient(0, 0, 0, 32); + gradient.addColorStop(0.0, '#014a84'); + gradient.addColorStop(0.5, '#0561a0'); + gradient.addColorStop(1.0, '#437ab6'); + context.fillStyle = gradient; + context.fillRect(0, 0, 1, 32); + + const skyMap = new THREE.CanvasTexture(canvas); + skyMap.colorSpace = THREE.SRGBColorSpace; + + const sky = new THREE.Mesh( + new THREE.SphereGeometry(10), + new THREE.MeshBasicMaterial({ map: skyMap, side: THREE.BackSide }), + ); + scene.add(sky); + + // Texture + + const texture = new THREE.Data3DTexture( + new Uint8Array(INITIAL_CLOUD_SIZE * INITIAL_CLOUD_SIZE * INITIAL_CLOUD_SIZE).fill(0), + INITIAL_CLOUD_SIZE, + INITIAL_CLOUD_SIZE, + INITIAL_CLOUD_SIZE, + ); + texture.format = THREE.RedFormat; + texture.minFilter = THREE.LinearFilter; + texture.magFilter = THREE.LinearFilter; + texture.unpackAlignment = 1; + texture.needsUpdate = true; + + cloudTexture = texture; + + // Material + + const vertexShader = /* glsl */ ` + in vec3 position; + + uniform mat4 modelMatrix; + uniform mat4 modelViewMatrix; + uniform mat4 projectionMatrix; + uniform vec3 cameraPos; + + out vec3 vOrigin; + out vec3 vDirection; + + void main() { + vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); + + vOrigin = vec3( inverse( modelMatrix ) * vec4( cameraPos, 1.0 ) ).xyz; + vDirection = position - vOrigin; + + gl_Position = projectionMatrix * mvPosition; + } + `; + + const fragmentShader = /* glsl */ ` + precision highp float; + precision highp sampler3D; + + uniform mat4 modelViewMatrix; + uniform mat4 projectionMatrix; + + in vec3 vOrigin; + in vec3 vDirection; + + out vec4 color; + + uniform vec3 base; + uniform sampler3D map; + + uniform float threshold; + uniform float range; + uniform float opacity; + uniform float steps; + uniform float frame; + + uint wang_hash(uint seed) + { + seed = (seed ^ 61u) ^ (seed >> 16u); + seed *= 9u; + seed = seed ^ (seed >> 4u); + seed *= 0x27d4eb2du; + seed = seed ^ (seed >> 15u); + return seed; + } + + float randomFloat(inout uint seed) + { + return float(wang_hash(seed)) / 4294967296.; + } + + vec2 hitBox( vec3 orig, vec3 dir ) { + const vec3 box_min = vec3( - 0.5 ); + const vec3 box_max = vec3( 0.5 ); + vec3 inv_dir = 1.0 / dir; + vec3 tmin_tmp = ( box_min - orig ) * inv_dir; + vec3 tmax_tmp = ( box_max - orig ) * inv_dir; + vec3 tmin = min( tmin_tmp, tmax_tmp ); + vec3 tmax = max( tmin_tmp, tmax_tmp ); + float t0 = max( tmin.x, max( tmin.y, tmin.z ) ); + float t1 = min( tmax.x, min( tmax.y, tmax.z ) ); + return vec2( t0, t1 ); + } + + float sample1( vec3 p ) { + return texture( map, p ).r; + } + + float shading( vec3 coord ) { + float step = 0.01; + return sample1( coord + vec3( - step ) ) - sample1( coord + vec3( step ) ); + } + + vec4 linearToSRGB( in vec4 value ) { + return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a ); + } + + void main(){ + vec3 rayDir = normalize( vDirection ); + vec2 bounds = hitBox( vOrigin, rayDir ); + + if ( bounds.x > bounds.y ) discard; + + bounds.x = max( bounds.x, 0.0 ); + + vec3 p = vOrigin + bounds.x * rayDir; + vec3 inc = 1.0 / abs( rayDir ); + float delta = min( inc.x, min( inc.y, inc.z ) ); + delta /= steps; + + // Jitter + + // Nice little seed from + // https://blog.demofox.org/2020/05/25/casual-shadertoy-path-tracing-1-basic-camera-diffuse-emissive/ + uint seed = uint( gl_FragCoord.x ) * uint( 1973 ) + uint( gl_FragCoord.y ) * uint( 9277 ) + uint( frame ) * uint( 26699 ); + vec3 size = vec3( textureSize( map, 0 ) ); + float randNum = randomFloat( seed ) * 2.0 - 1.0; + p += rayDir * randNum * ( 1.0 / size ); + + // + + vec4 ac = vec4( base, 0.0 ); + + for ( float t = bounds.x; t < bounds.y; t += delta ) { + + float d = sample1( p + 0.5 ); + + d = smoothstep( threshold - range, threshold + range, d ) * opacity; + + float col = shading( p + 0.5 ) * 3.0 + ( ( p.x + p.y ) * 0.25 ) + 0.2; + + ac.rgb += ( 1.0 - ac.a ) * d * col; + + ac.a += ( 1.0 - ac.a ) * d; + + if ( ac.a >= 0.95 ) break; + + p += rayDir * delta; + + } + + color = linearToSRGB( ac ); + + if ( color.a == 0.0 ) discard; + + } + `; + + const geometry = new THREE.BoxGeometry(1, 1, 1); + const material = new THREE.RawShaderMaterial({ + glslVersion: THREE.GLSL3, + uniforms: { + base: { value: new THREE.Color(0x798aa0) }, + map: { value: texture }, + cameraPos: { value: new THREE.Vector3() }, + threshold: { value: 0.25 }, + opacity: { value: 0.25 }, + range: { value: 0.1 }, + steps: { value: 100 }, + frame: { value: 0 }, + }, + vertexShader, + fragmentShader, + side: THREE.BackSide, + transparent: true, + }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + const parameters = { + threshold: 0.25, + opacity: 0.25, + range: 0.1, + steps: 100, + }; + + function update() { + material.uniforms.threshold.value = parameters.threshold; + material.uniforms.opacity.value = parameters.opacity; + material.uniforms.range.value = parameters.range; + material.uniforms.steps.value = parameters.steps; + } + + const gui = new GUI(); + gui.add(parameters, 'threshold', 0, 1, 0.01).onChange(update); + gui.add(parameters, 'opacity', 0, 1, 0.01).onChange(update); + gui.add(parameters, 'range', 0, 1, 0.01).onChange(update); + gui.add(parameters, 'steps', 0, 200, 1).onChange(update); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +let curr = 0; +const countPerRow = 4; +const countPerSlice = countPerRow * countPerRow; +const sliceCount = 4; +const totalCount = sliceCount * countPerSlice; +const margins = 8; + +const perElementPaddedSize = (INITIAL_CLOUD_SIZE - margins) / countPerRow; +const perElementSize = Math.floor((INITIAL_CLOUD_SIZE - 1) / countPerRow); + +function animate() { + const time = performance.now(); + if (time - prevTime > 1500.0 && curr < totalCount) { + const position = new THREE.Vector3( + Math.floor(curr % countPerRow) * perElementSize + margins * 0.5, + Math.floor((curr % countPerSlice) / countPerRow) * perElementSize + margins * 0.5, + Math.floor(curr / countPerSlice) * perElementSize + margins * 0.5, + ).floor(); + + const maxDimension = perElementPaddedSize - 1; + const box = new THREE.Box3( + new THREE.Vector3(0, 0, 0), + new THREE.Vector3(maxDimension, maxDimension, maxDimension), + ); + const scaleFactor = (Math.random() + 0.5) * 0.5; + const source = generateCloudTexture(perElementPaddedSize, scaleFactor); + + renderer.copyTextureToTexture3D(source, cloudTexture, box, position); + + prevTime = time; + + curr++; + } + + mesh.material.uniforms.cameraPos.value.copy(camera.position); + // mesh.rotation.y = - performance.now() / 7500; + + mesh.material.uniforms.frame.value++; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_tonemapping.ts b/examples-testing/examples/webgl_tonemapping.ts new file mode 100644 index 000000000..08115cf3e --- /dev/null +++ b/examples-testing/examples/webgl_tonemapping.ts @@ -0,0 +1,163 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +let mesh, renderer, scene, camera, controls; +let gui, + guiExposure = null; + +const params = { + exposure: 1.0, + toneMapping: 'AgX', + blurriness: 0.3, + intensity: 1.0, +}; + +const toneMappingOptions = { + None: THREE.NoToneMapping, + Linear: THREE.LinearToneMapping, + Reinhard: THREE.ReinhardToneMapping, + Cineon: THREE.CineonToneMapping, + ACESFilmic: THREE.ACESFilmicToneMapping, + AgX: THREE.AgXToneMapping, + Neutral: THREE.NeutralToneMapping, + Custom: THREE.CustomToneMapping, +}; + +init().catch(function (err) { + console.error(err); +}); + +async function init() { + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + renderer.toneMapping = toneMappingOptions[params.toneMapping]; + renderer.toneMappingExposure = params.exposure; + + // Set CustomToneMapping to Uncharted2 + // source: http://filmicworlds.com/blog/filmic-tonemapping-operators/ + + THREE.ShaderChunk.tonemapping_pars_fragment = THREE.ShaderChunk.tonemapping_pars_fragment.replace( + 'vec3 CustomToneMapping( vec3 color ) { return color; }', + + `#define Uncharted2Helper( x ) max( ( ( x * ( 0.15 * x + 0.10 * 0.50 ) + 0.20 * 0.02 ) / ( x * ( 0.15 * x + 0.50 ) + 0.20 * 0.30 ) ) - 0.02 / 0.30, vec3( 0.0 ) ) + + float toneMappingWhitePoint = 1.0; + + vec3 CustomToneMapping( vec3 color ) { + color *= toneMappingExposure; + return saturate( Uncharted2Helper( color ) / Uncharted2Helper( vec3( toneMappingWhitePoint ) ) ); + + }`, + ); + + scene = new THREE.Scene(); + scene.backgroundBlurriness = params.blurriness; + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 20); + camera.position.set(-1.8, 0.6, 2.7); + + controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use if there is no animation loop + controls.enableZoom = false; + controls.enablePan = false; + controls.target.set(0, 0, -0.2); + controls.update(); + + const rgbeLoader = new RGBELoader().setPath('textures/equirectangular/'); + + const gltfLoader = new GLTFLoader().setPath('models/gltf/DamagedHelmet/glTF/'); + + const [texture, gltf] = await Promise.all([ + rgbeLoader.loadAsync('venice_sunset_1k.hdr'), + gltfLoader.loadAsync('DamagedHelmet.gltf'), + ]); + + // environment + + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + scene.environment = texture; + + // model + + mesh = gltf.scene.getObjectByName('node_damagedHelmet_-6514'); + scene.add(mesh); + + render(); + + window.addEventListener('resize', onWindowResize); + + gui = new GUI(); + const toneMappingFolder = gui.addFolder('tone mapping'); + + toneMappingFolder + .add(params, 'toneMapping', Object.keys(toneMappingOptions)) + + .onChange(function () { + updateGUI(toneMappingFolder); + + renderer.toneMapping = toneMappingOptions[params.toneMapping]; + render(); + }); + + const backgroundFolder = gui.addFolder('background'); + + backgroundFolder + .add(params, 'blurriness', 0, 1) + + .onChange(function (value) { + scene.backgroundBlurriness = value; + render(); + }); + + backgroundFolder + .add(params, 'intensity', 0, 1) + + .onChange(function (value) { + scene.backgroundIntensity = value; + render(); + }); + + updateGUI(toneMappingFolder); + + gui.open(); +} + +function updateGUI(folder) { + if (guiExposure !== null) { + guiExposure.destroy(); + guiExposure = null; + } + + if (params.toneMapping !== 'None') { + guiExposure = folder + .add(params, 'exposure', 0, 2) + + .onChange(function () { + renderer.toneMappingExposure = params.exposure; + render(); + }); + } +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_ubo.ts b/examples-testing/examples/webgl_ubo.ts new file mode 100644 index 000000000..01064f115 --- /dev/null +++ b/examples-testing/examples/webgl_ubo.ts @@ -0,0 +1,137 @@ +import * as THREE from 'three'; + +let camera, scene, renderer, clock; + +init(); + +function init() { + const container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0, 0, 25); + + scene = new THREE.Scene(); + camera.lookAt(scene.position); + + clock = new THREE.Clock(); + + // geometry + + const geometry1 = new THREE.TetrahedronGeometry(); + const geometry2 = new THREE.BoxGeometry(); + + // texture + + const texture = new THREE.TextureLoader().load('textures/crate.gif'); + texture.colorSpace = THREE.SRGBColorSpace; + + // uniforms groups + + // Camera and lighting related data are perfect examples of using UBOs since you have to store these + // data just once. They can be shared across all shader programs. + + const cameraUniformsGroup = new THREE.UniformsGroup(); + cameraUniformsGroup.setName('ViewData'); + cameraUniformsGroup.add(new THREE.Uniform(camera.projectionMatrix)); // projection matrix + cameraUniformsGroup.add(new THREE.Uniform(camera.matrixWorldInverse)); // view matrix + + const lightingUniformsGroup = new THREE.UniformsGroup(); + lightingUniformsGroup.setName('LightingData'); + lightingUniformsGroup.add(new THREE.Uniform(new THREE.Vector3(0, 0, 10))); // light position + lightingUniformsGroup.add(new THREE.Uniform(new THREE.Color(0x7c7c7c))); // ambient color + lightingUniformsGroup.add(new THREE.Uniform(new THREE.Color(0xd5d5d5))); // diffuse color + lightingUniformsGroup.add(new THREE.Uniform(new THREE.Color(0xe7e7e7))); // specular color + lightingUniformsGroup.add(new THREE.Uniform(64)); // shininess + + // materials + + const material1 = new THREE.RawShaderMaterial({ + uniforms: { + modelMatrix: { value: null }, + normalMatrix: { value: null }, + color: { value: null }, + }, + vertexShader: document.getElementById('vertexShader1').textContent, + fragmentShader: document.getElementById('fragmentShader1').textContent, + glslVersion: THREE.GLSL3, + }); + + const material2 = new THREE.RawShaderMaterial({ + uniforms: { + modelMatrix: { value: null }, + diffuseMap: { value: null }, + }, + vertexShader: document.getElementById('vertexShader2').textContent, + fragmentShader: document.getElementById('fragmentShader2').textContent, + glslVersion: THREE.GLSL3, + }); + + // meshes + + for (let i = 0; i < 200; i++) { + let mesh; + + if (i % 2 === 0) { + mesh = new THREE.Mesh(geometry1, material1.clone()); + + mesh.material.uniformsGroups = [cameraUniformsGroup, lightingUniformsGroup]; + mesh.material.uniforms.modelMatrix.value = mesh.matrixWorld; + mesh.material.uniforms.normalMatrix.value = mesh.normalMatrix; + mesh.material.uniforms.color.value = new THREE.Color(0xffffff * Math.random()); + } else { + mesh = new THREE.Mesh(geometry2, material2.clone()); + + mesh.material.uniformsGroups = [cameraUniformsGroup, lightingUniformsGroup]; + mesh.material.uniforms.modelMatrix.value = mesh.matrixWorld; + mesh.material.uniforms.diffuseMap.value = texture; + } + + scene.add(mesh); + + const s = 1 + Math.random() * 0.5; + + mesh.scale.x = s; + mesh.scale.y = s; + mesh.scale.z = s; + + mesh.rotation.x = Math.random() * Math.PI; + mesh.rotation.y = Math.random() * Math.PI; + mesh.rotation.z = Math.random() * Math.PI; + + mesh.position.x = Math.random() * 40 - 20; + mesh.position.y = Math.random() * 40 - 20; + mesh.position.z = Math.random() * 20 - 10; + } + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize, false); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const delta = clock.getDelta(); + + scene.traverse(function (child) { + if (child.isMesh) { + child.rotation.x += delta * 0.5; + child.rotation.y += delta * 0.3; + } + }); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_ubo_arrays.ts b/examples-testing/examples/webgl_ubo_arrays.ts new file mode 100644 index 000000000..d846e1443 --- /dev/null +++ b/examples-testing/examples/webgl_ubo_arrays.ts @@ -0,0 +1,171 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, clock, stats; + +let lightingUniformsGroup, lightCenters; + +const container = document.getElementById('container'); + +const pointLightsMax = 300; + +const api = { + count: 200, +}; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0, 50, 50); + + scene = new THREE.Scene(); + camera.lookAt(scene.position); + + clock = new THREE.Clock(); + + // geometry + + const geometry = new THREE.SphereGeometry(); + + // uniforms groups + + lightingUniformsGroup = new THREE.UniformsGroup(); + lightingUniformsGroup.setName('LightingData'); + + const data = []; + const dataColors = []; + lightCenters = []; + + for (let i = 0; i < pointLightsMax; i++) { + const col = new THREE.Color(0xffffff * Math.random()).toArray(); + const x = Math.random() * 50 - 25; + const z = Math.random() * 50 - 25; + + data.push(new THREE.Uniform(new THREE.Vector4(x, 1, z, 0))); // light position + dataColors.push(new THREE.Uniform(new THREE.Vector4(col[0], col[1], col[2], 0))); // light color + + // Store the center positions + lightCenters.push({ x, z }); + } + + lightingUniformsGroup.add(data); // light position + lightingUniformsGroup.add(dataColors); // light position + lightingUniformsGroup.add(new THREE.Uniform(pointLightsMax)); // light position + + const cameraUniformsGroup = new THREE.UniformsGroup(); + cameraUniformsGroup.setName('ViewData'); + cameraUniformsGroup.add(new THREE.Uniform(camera.projectionMatrix)); // projection matrix + cameraUniformsGroup.add(new THREE.Uniform(camera.matrixWorldInverse)); // view matrix + + const material = new THREE.RawShaderMaterial({ + uniforms: { + modelMatrix: { value: null }, + normalMatrix: { value: null }, + }, + // uniformsGroups: [ cameraUniformsGroup, lightingUniformsGroup ], + name: 'Box', + defines: { + POINTLIGHTS_MAX: pointLightsMax, + }, + vertexShader: document.getElementById('vertexShader').textContent, + fragmentShader: document.getElementById('fragmentShader').textContent, + glslVersion: THREE.GLSL3, + }); + + const plane = new THREE.Mesh(new THREE.PlaneGeometry(100, 100), material.clone()); + plane.material.uniformsGroups = [cameraUniformsGroup, lightingUniformsGroup]; + plane.material.uniforms.modelMatrix.value = plane.matrixWorld; + plane.material.uniforms.normalMatrix.value = plane.normalMatrix; + plane.rotation.x = -Math.PI / 2; + plane.position.y = -1; + scene.add(plane); + + // meshes + const gridSize = { x: 10, y: 1, z: 10 }; + const spacing = 6; + + for (let i = 0; i < gridSize.x; i++) { + for (let j = 0; j < gridSize.y; j++) { + for (let k = 0; k < gridSize.z; k++) { + const mesh = new THREE.Mesh(geometry, material.clone()); + mesh.name = 'Sphere'; + mesh.material.uniformsGroups = [cameraUniformsGroup, lightingUniformsGroup]; + mesh.material.uniforms.modelMatrix.value = mesh.matrixWorld; + mesh.material.uniforms.normalMatrix.value = mesh.normalMatrix; + scene.add(mesh); + + mesh.position.x = i * spacing - (gridSize.x * spacing) / 2; + mesh.position.y = 0; + mesh.position.z = k * spacing - (gridSize.z * spacing) / 2; + } + } + } + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize, false); + + // controls + + const controls = new OrbitControls(camera, renderer.domElement); + controls.enablePan = false; + + // stats + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // gui + const gui = new GUI(); + gui.add(api, 'count', 1, pointLightsMax) + .step(1) + .onChange(function () { + lightingUniformsGroup.uniforms[2].value = api.count; + }); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const elapsedTime = clock.getElapsedTime(); + + const lights = lightingUniformsGroup.uniforms[0]; + + // Parameters for circular movement + const radius = 5; // Smaller radius for individual circular movements + const speed = 0.5; // Speed of rotation + + // Update each light's position + for (let i = 0; i < lights.length; i++) { + const light = lights[i]; + const center = lightCenters[i]; + + // Calculate circular movement around the light's center + const angle = speed * elapsedTime + i * 0.5; // Phase difference for each light + const x = center.x + Math.sin(angle) * radius; + const z = center.z + Math.cos(angle) * radius; + + // Update the light's position + light.value.set(x, 1, z, 0); + } + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgl_video_kinect.ts b/examples-testing/examples/webgl_video_kinect.ts new file mode 100644 index 000000000..4f0e2f113 --- /dev/null +++ b/examples-testing/examples/webgl_video_kinect.ts @@ -0,0 +1,113 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let scene, camera, renderer; +let geometry, mesh, material; +let mouse, center; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + const info = document.createElement('div'); + info.id = 'info'; + info.innerHTML = 'three.js - kinect'; + document.body.appendChild(info); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.set(0, 0, 500); + + scene = new THREE.Scene(); + center = new THREE.Vector3(); + center.z = -1000; + + const video = document.getElementById('video'); + + const texture = new THREE.VideoTexture(video); + texture.minFilter = THREE.NearestFilter; + + const width = 640, + height = 480; + const nearClipping = 850, + farClipping = 4000; + + geometry = new THREE.BufferGeometry(); + + const vertices = new Float32Array(width * height * 3); + + for (let i = 0, j = 0, l = vertices.length; i < l; i += 3, j++) { + vertices[i] = j % width; + vertices[i + 1] = Math.floor(j / width); + } + + geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3)); + + material = new THREE.ShaderMaterial({ + uniforms: { + map: { value: texture }, + width: { value: width }, + height: { value: height }, + nearClipping: { value: nearClipping }, + farClipping: { value: farClipping }, + + pointSize: { value: 2 }, + zOffset: { value: 1000 }, + }, + vertexShader: document.getElementById('vs').textContent, + fragmentShader: document.getElementById('fs').textContent, + blending: THREE.AdditiveBlending, + depthTest: false, + depthWrite: false, + transparent: true, + }); + + mesh = new THREE.Points(geometry, material); + scene.add(mesh); + + const gui = new GUI(); + gui.add(material.uniforms.nearClipping, 'value', 1, 10000, 1.0).name('nearClipping'); + gui.add(material.uniforms.farClipping, 'value', 1, 10000, 1.0).name('farClipping'); + gui.add(material.uniforms.pointSize, 'value', 1, 10, 1.0).name('pointSize'); + gui.add(material.uniforms.zOffset, 'value', 0, 4000, 1.0).name('zOffset'); + + video.play(); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + mouse = new THREE.Vector3(0, 0, 1); + + document.addEventListener('mousemove', onDocumentMouseMove); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onDocumentMouseMove(event) { + mouse.x = (event.clientX - window.innerWidth / 2) * 8; + mouse.y = (event.clientY - window.innerHeight / 2) * 8; +} + +function animate() { + camera.position.x += (mouse.x - camera.position.x) * 0.05; + camera.position.y += (-mouse.y - camera.position.y) * 0.05; + camera.lookAt(center); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_video_panorama_equirectangular.ts b/examples-testing/examples/webgl_video_panorama_equirectangular.ts new file mode 100644 index 000000000..866eca16a --- /dev/null +++ b/examples-testing/examples/webgl_video_panorama_equirectangular.ts @@ -0,0 +1,95 @@ +import * as THREE from 'three'; + +let camera, scene, renderer; + +let isUserInteracting = false, + lon = 0, + lat = 0, + phi = 0, + theta = 0, + onPointerDownPointerX = 0, + onPointerDownPointerY = 0, + onPointerDownLon = 0, + onPointerDownLat = 0; + +const distance = 0.5; + +init(); + +function init() { + const container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.25, 10); + + scene = new THREE.Scene(); + + const geometry = new THREE.SphereGeometry(5, 60, 40); + // invert the geometry on the x-axis so that all of the faces point inward + geometry.scale(-1, 1, 1); + + const video = document.getElementById('video'); + video.play(); + + const texture = new THREE.VideoTexture(video); + texture.colorSpace = THREE.SRGBColorSpace; + const material = new THREE.MeshBasicMaterial({ map: texture }); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + document.addEventListener('pointerdown', onPointerDown); + document.addEventListener('pointermove', onPointerMove); + document.addEventListener('pointerup', onPointerUp); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onPointerDown(event) { + isUserInteracting = true; + + onPointerDownPointerX = event.clientX; + onPointerDownPointerY = event.clientY; + + onPointerDownLon = lon; + onPointerDownLat = lat; +} + +function onPointerMove(event) { + if (isUserInteracting === true) { + lon = (onPointerDownPointerX - event.clientX) * 0.1 + onPointerDownLon; + lat = (onPointerDownPointerY - event.clientY) * 0.1 + onPointerDownLat; + } +} + +function onPointerUp() { + isUserInteracting = false; +} + +function animate() { + lat = Math.max(-85, Math.min(85, lat)); + phi = THREE.MathUtils.degToRad(90 - lat); + theta = THREE.MathUtils.degToRad(lon); + + camera.position.x = distance * Math.sin(phi) * Math.cos(theta); + camera.position.y = distance * Math.cos(phi); + camera.position.z = distance * Math.sin(phi) * Math.sin(theta); + + camera.lookAt(0, 0, 0); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_volume_cloud.ts b/examples-testing/examples/webgl_volume_cloud.ts new file mode 100644 index 000000000..77dd8de43 --- /dev/null +++ b/examples-testing/examples/webgl_volume_cloud.ts @@ -0,0 +1,279 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let renderer, scene, camera; +let mesh; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0, 0, 1.5); + + new OrbitControls(camera, renderer.domElement); + + // Sky + + const canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 32; + + const context = canvas.getContext('2d'); + const gradient = context.createLinearGradient(0, 0, 0, 32); + gradient.addColorStop(0.0, '#014a84'); + gradient.addColorStop(0.5, '#0561a0'); + gradient.addColorStop(1.0, '#437ab6'); + context.fillStyle = gradient; + context.fillRect(0, 0, 1, 32); + + const skyMap = new THREE.CanvasTexture(canvas); + skyMap.colorSpace = THREE.SRGBColorSpace; + + const sky = new THREE.Mesh( + new THREE.SphereGeometry(10), + new THREE.MeshBasicMaterial({ map: skyMap, side: THREE.BackSide }), + ); + scene.add(sky); + + // Texture + + const size = 128; + const data = new Uint8Array(size * size * size); + + let i = 0; + const scale = 0.05; + const perlin = new ImprovedNoise(); + const vector = new THREE.Vector3(); + + for (let z = 0; z < size; z++) { + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + const d = + 1.0 - + vector + .set(x, y, z) + .subScalar(size / 2) + .divideScalar(size) + .length(); + data[i] = (128 + 128 * perlin.noise((x * scale) / 1.5, y * scale, (z * scale) / 1.5)) * d * d; + i++; + } + } + } + + const texture = new THREE.Data3DTexture(data, size, size, size); + texture.format = THREE.RedFormat; + texture.minFilter = THREE.LinearFilter; + texture.magFilter = THREE.LinearFilter; + texture.unpackAlignment = 1; + texture.needsUpdate = true; + + // Material + + const vertexShader = /* glsl */ ` + in vec3 position; + + uniform mat4 modelMatrix; + uniform mat4 modelViewMatrix; + uniform mat4 projectionMatrix; + uniform vec3 cameraPos; + + out vec3 vOrigin; + out vec3 vDirection; + + void main() { + vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); + + vOrigin = vec3( inverse( modelMatrix ) * vec4( cameraPos, 1.0 ) ).xyz; + vDirection = position - vOrigin; + + gl_Position = projectionMatrix * mvPosition; + } + `; + + const fragmentShader = /* glsl */ ` + precision highp float; + precision highp sampler3D; + + uniform mat4 modelViewMatrix; + uniform mat4 projectionMatrix; + + in vec3 vOrigin; + in vec3 vDirection; + + out vec4 color; + + uniform vec3 base; + uniform sampler3D map; + + uniform float threshold; + uniform float range; + uniform float opacity; + uniform float steps; + uniform float frame; + + uint wang_hash(uint seed) + { + seed = (seed ^ 61u) ^ (seed >> 16u); + seed *= 9u; + seed = seed ^ (seed >> 4u); + seed *= 0x27d4eb2du; + seed = seed ^ (seed >> 15u); + return seed; + } + + float randomFloat(inout uint seed) + { + return float(wang_hash(seed)) / 4294967296.; + } + + vec2 hitBox( vec3 orig, vec3 dir ) { + const vec3 box_min = vec3( - 0.5 ); + const vec3 box_max = vec3( 0.5 ); + vec3 inv_dir = 1.0 / dir; + vec3 tmin_tmp = ( box_min - orig ) * inv_dir; + vec3 tmax_tmp = ( box_max - orig ) * inv_dir; + vec3 tmin = min( tmin_tmp, tmax_tmp ); + vec3 tmax = max( tmin_tmp, tmax_tmp ); + float t0 = max( tmin.x, max( tmin.y, tmin.z ) ); + float t1 = min( tmax.x, min( tmax.y, tmax.z ) ); + return vec2( t0, t1 ); + } + + float sample1( vec3 p ) { + return texture( map, p ).r; + } + + float shading( vec3 coord ) { + float step = 0.01; + return sample1( coord + vec3( - step ) ) - sample1( coord + vec3( step ) ); + } + + vec4 linearToSRGB( in vec4 value ) { + return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a ); + } + + void main(){ + vec3 rayDir = normalize( vDirection ); + vec2 bounds = hitBox( vOrigin, rayDir ); + + if ( bounds.x > bounds.y ) discard; + + bounds.x = max( bounds.x, 0.0 ); + + vec3 p = vOrigin + bounds.x * rayDir; + vec3 inc = 1.0 / abs( rayDir ); + float delta = min( inc.x, min( inc.y, inc.z ) ); + delta /= steps; + + // Jitter + + // Nice little seed from + // https://blog.demofox.org/2020/05/25/casual-shadertoy-path-tracing-1-basic-camera-diffuse-emissive/ + uint seed = uint( gl_FragCoord.x ) * uint( 1973 ) + uint( gl_FragCoord.y ) * uint( 9277 ) + uint( frame ) * uint( 26699 ); + vec3 size = vec3( textureSize( map, 0 ) ); + float randNum = randomFloat( seed ) * 2.0 - 1.0; + p += rayDir * randNum * ( 1.0 / size ); + + // + + vec4 ac = vec4( base, 0.0 ); + + for ( float t = bounds.x; t < bounds.y; t += delta ) { + + float d = sample1( p + 0.5 ); + + d = smoothstep( threshold - range, threshold + range, d ) * opacity; + + float col = shading( p + 0.5 ) * 3.0 + ( ( p.x + p.y ) * 0.25 ) + 0.2; + + ac.rgb += ( 1.0 - ac.a ) * d * col; + + ac.a += ( 1.0 - ac.a ) * d; + + if ( ac.a >= 0.95 ) break; + + p += rayDir * delta; + + } + + color = linearToSRGB( ac ); + + if ( color.a == 0.0 ) discard; + + } + `; + + const geometry = new THREE.BoxGeometry(1, 1, 1); + const material = new THREE.RawShaderMaterial({ + glslVersion: THREE.GLSL3, + uniforms: { + base: { value: new THREE.Color(0x798aa0) }, + map: { value: texture }, + cameraPos: { value: new THREE.Vector3() }, + threshold: { value: 0.25 }, + opacity: { value: 0.25 }, + range: { value: 0.1 }, + steps: { value: 100 }, + frame: { value: 0 }, + }, + vertexShader, + fragmentShader, + side: THREE.BackSide, + transparent: true, + }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + const parameters = { + threshold: 0.25, + opacity: 0.25, + range: 0.1, + steps: 100, + }; + + function update() { + material.uniforms.threshold.value = parameters.threshold; + material.uniforms.opacity.value = parameters.opacity; + material.uniforms.range.value = parameters.range; + material.uniforms.steps.value = parameters.steps; + } + + const gui = new GUI(); + gui.add(parameters, 'threshold', 0, 1, 0.01).onChange(update); + gui.add(parameters, 'opacity', 0, 1, 0.01).onChange(update); + gui.add(parameters, 'range', 0, 1, 0.01).onChange(update); + gui.add(parameters, 'steps', 0, 200, 1).onChange(update); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + mesh.material.uniforms.cameraPos.value.copy(camera.position); + mesh.rotation.y = -performance.now() / 7500; + + mesh.material.uniforms.frame.value++; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_volume_instancing.ts b/examples-testing/examples/webgl_volume_instancing.ts new file mode 100644 index 000000000..bf90eeea9 --- /dev/null +++ b/examples-testing/examples/webgl_volume_instancing.ts @@ -0,0 +1,192 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { VOXLoader, VOXData3DTexture } from 'three/addons/loaders/VOXLoader.js'; + +let renderer, scene, camera, controls, clock; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000); + camera.position.set(0, 0, 4); + + controls = new OrbitControls(camera, renderer.domElement); + controls.autoRotate = true; + controls.autoRotateSpeed = -1.0; + controls.enableDamping = true; + + clock = new THREE.Clock(); + + // Material + + const vertexShader = /* glsl */ ` + in vec3 position; + in mat4 instanceMatrix; + + uniform mat4 modelMatrix; + uniform mat4 modelViewMatrix; + uniform mat4 projectionMatrix; + uniform vec3 cameraPos; + + out vec3 vOrigin; + out vec3 vDirection; + + void main() { + vec4 mvPosition = modelViewMatrix * instanceMatrix * vec4( position, 1.0 ); + + vOrigin = vec3( inverse( instanceMatrix * modelMatrix ) * vec4( cameraPos, 1.0 ) ).xyz; + vDirection = position - vOrigin; + + gl_Position = projectionMatrix * mvPosition; + } + `; + + const fragmentShader = /* glsl */ ` + precision highp float; + precision highp sampler3D; + + uniform mat4 modelViewMatrix; + uniform mat4 projectionMatrix; + + in vec3 vOrigin; + in vec3 vDirection; + + out vec4 color; + + uniform sampler3D map; + + uniform float threshold; + uniform float steps; + + vec2 hitBox( vec3 orig, vec3 dir ) { + const vec3 box_min = vec3( - 0.5 ); + const vec3 box_max = vec3( 0.5 ); + vec3 inv_dir = 1.0 / dir; + vec3 tmin_tmp = ( box_min - orig ) * inv_dir; + vec3 tmax_tmp = ( box_max - orig ) * inv_dir; + vec3 tmin = min( tmin_tmp, tmax_tmp ); + vec3 tmax = max( tmin_tmp, tmax_tmp ); + float t0 = max( tmin.x, max( tmin.y, tmin.z ) ); + float t1 = min( tmax.x, min( tmax.y, tmax.z ) ); + return vec2( t0, t1 ); + } + + float sample1( vec3 p ) { + return texture( map, p ).r; + } + + #define epsilon .0001 + + vec3 normal( vec3 coord ) { + if ( coord.x < epsilon ) return vec3( 1.0, 0.0, 0.0 ); + if ( coord.y < epsilon ) return vec3( 0.0, 1.0, 0.0 ); + if ( coord.z < epsilon ) return vec3( 0.0, 0.0, 1.0 ); + if ( coord.x > 1.0 - epsilon ) return vec3( - 1.0, 0.0, 0.0 ); + if ( coord.y > 1.0 - epsilon ) return vec3( 0.0, - 1.0, 0.0 ); + if ( coord.z > 1.0 - epsilon ) return vec3( 0.0, 0.0, - 1.0 ); + + float step = 0.01; + float x = sample1( coord + vec3( - step, 0.0, 0.0 ) ) - sample1( coord + vec3( step, 0.0, 0.0 ) ); + float y = sample1( coord + vec3( 0.0, - step, 0.0 ) ) - sample1( coord + vec3( 0.0, step, 0.0 ) ); + float z = sample1( coord + vec3( 0.0, 0.0, - step ) ) - sample1( coord + vec3( 0.0, 0.0, step ) ); + + return normalize( vec3( x, y, z ) ); + } + + void main(){ + + vec3 rayDir = normalize( vDirection ); + vec2 bounds = hitBox( vOrigin, rayDir ); + + if ( bounds.x > bounds.y ) discard; + + bounds.x = max( bounds.x, 0.0 ); + + vec3 p = vOrigin + bounds.x * rayDir; + vec3 inc = 1.0 / abs( rayDir ); + float delta = min( inc.x, min( inc.y, inc.z ) ); + delta /= 50.0; + + for ( float t = bounds.x; t < bounds.y; t += delta ) { + + float d = sample1( p + 0.5 ); + + if ( d > 0.5 ) { + + color.rgb = p * 2.0; // normal( p + 0.5 ); // * 0.5 + ( p * 1.5 + 0.25 ); + color.a = 1.; + break; + + } + + p += rayDir * delta; + + } + + if ( color.a == 0.0 ) discard; + + } + `; + + const loader = new VOXLoader(); + loader.load('models/vox/menger.vox', function (chunks) { + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + + const geometry = new THREE.BoxGeometry(1, 1, 1); + const material = new THREE.RawShaderMaterial({ + glslVersion: THREE.GLSL3, + uniforms: { + map: { value: new VOXData3DTexture(chunk) }, + cameraPos: { value: new THREE.Vector3() }, + }, + vertexShader, + fragmentShader, + side: THREE.BackSide, + }); + + const mesh = new THREE.InstancedMesh(geometry, material, 50000); + mesh.onBeforeRender = function () { + this.material.uniforms.cameraPos.value.copy(camera.position); + }; + + const transform = new THREE.Object3D(); + + for (let i = 0; i < mesh.count; i++) { + transform.position.random().subScalar(0.5).multiplyScalar(150); + transform.rotation.x = Math.random() * Math.PI; + transform.rotation.y = Math.random() * Math.PI; + transform.rotation.z = Math.random() * Math.PI; + transform.updateMatrix(); + + mesh.setMatrixAt(i, transform.matrix); + } + + scene.add(mesh); + } + }); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const delta = clock.getDelta(); + controls.update(delta); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_volume_perlin.ts b/examples-testing/examples/webgl_volume_perlin.ts new file mode 100644 index 000000000..a98f9a682 --- /dev/null +++ b/examples-testing/examples/webgl_volume_perlin.ts @@ -0,0 +1,208 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let renderer, scene, camera; +let mesh; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0, 0, 2); + + new OrbitControls(camera, renderer.domElement); + + // Texture + + const size = 128; + const data = new Uint8Array(size * size * size); + + let i = 0; + const perlin = new ImprovedNoise(); + const vector = new THREE.Vector3(); + + for (let z = 0; z < size; z++) { + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + vector.set(x, y, z).divideScalar(size); + + const d = perlin.noise(vector.x * 6.5, vector.y * 6.5, vector.z * 6.5); + + data[i++] = d * 128 + 128; + } + } + } + + const texture = new THREE.Data3DTexture(data, size, size, size); + texture.format = THREE.RedFormat; + texture.minFilter = THREE.LinearFilter; + texture.magFilter = THREE.LinearFilter; + texture.unpackAlignment = 1; + texture.needsUpdate = true; + + // Material + + const vertexShader = /* glsl */ ` + in vec3 position; + + uniform mat4 modelMatrix; + uniform mat4 modelViewMatrix; + uniform mat4 projectionMatrix; + uniform vec3 cameraPos; + + out vec3 vOrigin; + out vec3 vDirection; + + void main() { + vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); + + vOrigin = vec3( inverse( modelMatrix ) * vec4( cameraPos, 1.0 ) ).xyz; + vDirection = position - vOrigin; + + gl_Position = projectionMatrix * mvPosition; + } + `; + + const fragmentShader = /* glsl */ ` + precision highp float; + precision highp sampler3D; + + uniform mat4 modelViewMatrix; + uniform mat4 projectionMatrix; + + in vec3 vOrigin; + in vec3 vDirection; + + out vec4 color; + + uniform sampler3D map; + + uniform float threshold; + uniform float steps; + + vec2 hitBox( vec3 orig, vec3 dir ) { + const vec3 box_min = vec3( - 0.5 ); + const vec3 box_max = vec3( 0.5 ); + vec3 inv_dir = 1.0 / dir; + vec3 tmin_tmp = ( box_min - orig ) * inv_dir; + vec3 tmax_tmp = ( box_max - orig ) * inv_dir; + vec3 tmin = min( tmin_tmp, tmax_tmp ); + vec3 tmax = max( tmin_tmp, tmax_tmp ); + float t0 = max( tmin.x, max( tmin.y, tmin.z ) ); + float t1 = min( tmax.x, min( tmax.y, tmax.z ) ); + return vec2( t0, t1 ); + } + + float sample1( vec3 p ) { + return texture( map, p ).r; + } + + #define epsilon .0001 + + vec3 normal( vec3 coord ) { + if ( coord.x < epsilon ) return vec3( 1.0, 0.0, 0.0 ); + if ( coord.y < epsilon ) return vec3( 0.0, 1.0, 0.0 ); + if ( coord.z < epsilon ) return vec3( 0.0, 0.0, 1.0 ); + if ( coord.x > 1.0 - epsilon ) return vec3( - 1.0, 0.0, 0.0 ); + if ( coord.y > 1.0 - epsilon ) return vec3( 0.0, - 1.0, 0.0 ); + if ( coord.z > 1.0 - epsilon ) return vec3( 0.0, 0.0, - 1.0 ); + + float step = 0.01; + float x = sample1( coord + vec3( - step, 0.0, 0.0 ) ) - sample1( coord + vec3( step, 0.0, 0.0 ) ); + float y = sample1( coord + vec3( 0.0, - step, 0.0 ) ) - sample1( coord + vec3( 0.0, step, 0.0 ) ); + float z = sample1( coord + vec3( 0.0, 0.0, - step ) ) - sample1( coord + vec3( 0.0, 0.0, step ) ); + + return normalize( vec3( x, y, z ) ); + } + + void main(){ + + vec3 rayDir = normalize( vDirection ); + vec2 bounds = hitBox( vOrigin, rayDir ); + + if ( bounds.x > bounds.y ) discard; + + bounds.x = max( bounds.x, 0.0 ); + + vec3 p = vOrigin + bounds.x * rayDir; + vec3 inc = 1.0 / abs( rayDir ); + float delta = min( inc.x, min( inc.y, inc.z ) ); + delta /= steps; + + for ( float t = bounds.x; t < bounds.y; t += delta ) { + + float d = sample1( p + 0.5 ); + + if ( d > threshold ) { + + color.rgb = normal( p + 0.5 ) * 0.5 + ( p * 1.5 + 0.25 ); + color.a = 1.; + break; + + } + + p += rayDir * delta; + + } + + if ( color.a == 0.0 ) discard; + + } + `; + + const geometry = new THREE.BoxGeometry(1, 1, 1); + const material = new THREE.RawShaderMaterial({ + glslVersion: THREE.GLSL3, + uniforms: { + map: { value: texture }, + cameraPos: { value: new THREE.Vector3() }, + threshold: { value: 0.6 }, + steps: { value: 200 }, + }, + vertexShader, + fragmentShader, + side: THREE.BackSide, + }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + const parameters = { threshold: 0.6, steps: 200 }; + + function update() { + material.uniforms.threshold.value = parameters.threshold; + material.uniforms.steps.value = parameters.steps; + } + + const gui = new GUI(); + gui.add(parameters, 'threshold', 0, 1, 0.01).onChange(update); + gui.add(parameters, 'steps', 0, 300, 1).onChange(update); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + mesh.material.uniforms.cameraPos.value.copy(camera.position); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_water.ts b/examples-testing/examples/webgl_water.ts new file mode 100644 index 000000000..496a5f855 --- /dev/null +++ b/examples-testing/examples/webgl_water.ts @@ -0,0 +1,162 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { Water } from 'three/addons/objects/Water2.js'; + +let scene, camera, clock, renderer, water; + +let torusKnot; + +const params = { + color: '#ffffff', + scale: 4, + flowX: 1, + flowY: 1, +}; + +init(); + +function init() { + // scene + + scene = new THREE.Scene(); + + // camera + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200); + camera.position.set(-15, 7, 15); + camera.lookAt(scene.position); + + // clock + + clock = new THREE.Clock(); + + // mesh + + const torusKnotGeometry = new THREE.TorusKnotGeometry(3, 1, 256, 32); + const torusKnotMaterial = new THREE.MeshNormalMaterial(); + + torusKnot = new THREE.Mesh(torusKnotGeometry, torusKnotMaterial); + torusKnot.position.y = 4; + torusKnot.scale.set(0.5, 0.5, 0.5); + scene.add(torusKnot); + + // ground + + const groundGeometry = new THREE.PlaneGeometry(20, 20); + const groundMaterial = new THREE.MeshStandardMaterial({ roughness: 0.8, metalness: 0.4 }); + const ground = new THREE.Mesh(groundGeometry, groundMaterial); + ground.rotation.x = Math.PI * -0.5; + scene.add(ground); + + const textureLoader = new THREE.TextureLoader(); + textureLoader.load('textures/hardwood2_diffuse.jpg', function (map) { + map.wrapS = THREE.RepeatWrapping; + map.wrapT = THREE.RepeatWrapping; + map.anisotropy = 16; + map.repeat.set(4, 4); + map.colorSpace = THREE.SRGBColorSpace; + groundMaterial.map = map; + groundMaterial.needsUpdate = true; + }); + + // water + + const waterGeometry = new THREE.PlaneGeometry(20, 20); + + water = new Water(waterGeometry, { + color: params.color, + scale: params.scale, + flowDirection: new THREE.Vector2(params.flowX, params.flowY), + textureWidth: 1024, + textureHeight: 1024, + }); + + water.position.y = 1; + water.rotation.x = Math.PI * -0.5; + scene.add(water); + + // skybox + + const cubeTextureLoader = new THREE.CubeTextureLoader(); + cubeTextureLoader.setPath('textures/cube/Park2/'); + + const cubeTexture = cubeTextureLoader.load([ + 'posx.jpg', + 'negx.jpg', + 'posy.jpg', + 'negy.jpg', + 'posz.jpg', + 'negz.jpg', + ]); + + scene.background = cubeTexture; + + // light + + const ambientLight = new THREE.AmbientLight(0xe7e7e7, 1.2); + scene.add(ambientLight); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 2); + directionalLight.position.set(-1, 1, 1); + scene.add(directionalLight); + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // gui + + const gui = new GUI(); + + gui.addColor(params, 'color').onChange(function (value) { + water.material.uniforms['color'].value.set(value); + }); + gui.add(params, 'scale', 1, 10).onChange(function (value) { + water.material.uniforms['config'].value.w = value; + }); + gui.add(params, 'flowX', -1, 1) + .step(0.01) + .onChange(function (value) { + water.material.uniforms['flowDirection'].value.x = value; + water.material.uniforms['flowDirection'].value.normalize(); + }); + gui.add(params, 'flowY', -1, 1) + .step(0.01) + .onChange(function (value) { + water.material.uniforms['flowDirection'].value.y = value; + water.material.uniforms['flowDirection'].value.normalize(); + }); + + gui.open(); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 5; + controls.maxDistance = 50; + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const delta = clock.getDelta(); + + torusKnot.rotation.x += delta; + torusKnot.rotation.y += delta * 0.5; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgl_water_flowmap.ts b/examples-testing/examples/webgl_water_flowmap.ts new file mode 100644 index 000000000..d0255e431 --- /dev/null +++ b/examples-testing/examples/webgl_water_flowmap.ts @@ -0,0 +1,100 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { Water } from 'three/addons/objects/Water2.js'; + +let scene, camera, renderer, water; + +init(); + +function init() { + // scene + + scene = new THREE.Scene(); + + // camera + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200); + camera.position.set(0, 25, 0); + camera.lookAt(scene.position); + + // ground + + const groundGeometry = new THREE.PlaneGeometry(20, 20, 10, 10); + const groundMaterial = new THREE.MeshBasicMaterial({ color: 0xe7e7e7 }); + const ground = new THREE.Mesh(groundGeometry, groundMaterial); + ground.rotation.x = Math.PI * -0.5; + scene.add(ground); + + const textureLoader = new THREE.TextureLoader(); + textureLoader.load('textures/floors/FloorsCheckerboard_S_Diffuse.jpg', function (map) { + map.wrapS = THREE.RepeatWrapping; + map.wrapT = THREE.RepeatWrapping; + map.anisotropy = 16; + map.repeat.set(4, 4); + map.colorSpace = THREE.SRGBColorSpace; + groundMaterial.map = map; + groundMaterial.needsUpdate = true; + }); + + // water + + const waterGeometry = new THREE.PlaneGeometry(20, 20); + const flowMap = textureLoader.load('textures/water/Water_1_M_Flow.jpg'); + + water = new Water(waterGeometry, { + scale: 2, + textureWidth: 1024, + textureHeight: 1024, + flowMap: flowMap, + }); + + water.position.y = 1; + water.rotation.x = Math.PI * -0.5; + scene.add(water); + + // flow map helper + + const helperGeometry = new THREE.PlaneGeometry(20, 20); + const helperMaterial = new THREE.MeshBasicMaterial({ map: flowMap }); + const helper = new THREE.Mesh(helperGeometry, helperMaterial); + helper.position.y = 1.01; + helper.rotation.x = Math.PI * -0.5; + helper.visible = false; + scene.add(helper); + + // renderer + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + const gui = new GUI(); + gui.add(helper, 'visible').name('Show Flow Map'); + gui.open(); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 5; + controls.maxDistance = 50; + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_backdrop_area.ts b/examples-testing/examples/webgpu_backdrop_area.ts new file mode 100644 index 000000000..208bb15ee --- /dev/null +++ b/examples-testing/examples/webgpu_backdrop_area.ts @@ -0,0 +1,162 @@ +import * as THREE from 'three'; +import { + color, + linearDepth, + viewportLinearDepth, + viewportSharedTexture, + viewportMipTexture, + viewportTopLeft, + checker, + uv, + modelScale, +} from 'three/tsl'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer; +let mixer, clock; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.25, 25); + camera.position.set(3, 2, 3); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x777777); + camera.lookAt(0, 1, 0); + + clock = new THREE.Clock(); + + const light = new THREE.PointLight(0xffffff, 50); + camera.add(light); + scene.add(camera); + + const ambient = new THREE.AmbientLight(0x4466ff, 1); + scene.add(ambient); + + // model + + const loader = new GLTFLoader(); + loader.load('models/gltf/Michelle.glb', function (gltf) { + const object = gltf.scene; + mixer = new THREE.AnimationMixer(object); + + const action = mixer.clipAction(gltf.animations[0]); + action.play(); + + scene.add(object); + }); + + // volume + + // compare depth from viewportLinearDepth with linearDepth() to create a distance field + // viewportLinearDepth return the linear depth of the scene + // linearDepth() returns the linear depth of the mesh + const depthDistance = viewportLinearDepth.distance(linearDepth()); + + const depthAlphaNode = depthDistance.oneMinus().smoothstep(0.9, 2).mul(10).saturate(); + const depthBlurred = viewportMipTexture().bicubic( + depthDistance + .smoothstep(0, 0.6) + .mul(40 * 5) + .clamp(0, 5), + ); + + const blurredBlur = new THREE.MeshBasicNodeMaterial(); + blurredBlur.backdropNode = depthBlurred.add(depthAlphaNode.mix(color(0x0066ff), 0)); + blurredBlur.transparent = true; + blurredBlur.side = THREE.DoubleSide; + + const volumeMaterial = new THREE.MeshBasicNodeMaterial(); + volumeMaterial.colorNode = color(0x0066ff); + volumeMaterial.backdropNode = viewportSharedTexture(); + volumeMaterial.backdropAlphaNode = depthAlphaNode; + volumeMaterial.transparent = true; + volumeMaterial.side = THREE.DoubleSide; + + const depthMaterial = new THREE.MeshBasicNodeMaterial(); + depthMaterial.backdropNode = depthAlphaNode; + depthMaterial.transparent = true; + depthMaterial.side = THREE.DoubleSide; + + const bicubicMaterial = new THREE.MeshBasicNodeMaterial(); + bicubicMaterial.backdropNode = viewportMipTexture().bicubic(5); // @TODO: Move to alpha value [ 0, 1 ] + bicubicMaterial.backdropAlphaNode = checker(uv().mul(3).mul(modelScale.xy)); + bicubicMaterial.opacityNode = bicubicMaterial.backdropAlphaNode; + bicubicMaterial.transparent = true; + bicubicMaterial.side = THREE.DoubleSide; + + const pixelMaterial = new THREE.MeshBasicNodeMaterial(); + pixelMaterial.backdropNode = viewportSharedTexture(viewportTopLeft.mul(100).floor().div(100)); + pixelMaterial.transparent = true; + + // box / floor + + const box = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), volumeMaterial); + box.position.set(0, 1, 0); + scene.add(box); + + const floor = new THREE.Mesh( + new THREE.BoxGeometry(1.99, 0.01, 1.99), + new THREE.MeshBasicNodeMaterial({ color: 0x333333 }), + ); + floor.position.set(0, 0, 0); + scene.add(floor); + + // renderer + + renderer = new THREE.WebGPURenderer(/*{ antialias: true }*/); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.LinearToneMapping; + renderer.toneMappingExposure = 0.2; + document.body.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 1, 0); + controls.update(); + + window.addEventListener('resize', onWindowResize); + + // gui + + const materials = { + blurred: blurredBlur, + volume: volumeMaterial, + depth: depthMaterial, + bicubic: bicubicMaterial, + pixel: pixelMaterial, + }; + + const gui = new GUI(); + const options = { material: 'blurred' }; + + box.material = materials[options.material]; + + gui.add(box.scale, 'x', 0.1, 2, 0.01); + gui.add(box.scale, 'z', 0.1, 2, 0.01); + gui.add(options, 'material', Object.keys(materials)).onChange(name => { + box.material = materials[name]; + }); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const delta = clock.getDelta(); + + if (mixer) mixer.update(delta); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_camera_logarithmicdepthbuffer.ts b/examples-testing/examples/webgpu_camera_logarithmicdepthbuffer.ts new file mode 100644 index 000000000..155276322 --- /dev/null +++ b/examples-testing/examples/webgpu_camera_logarithmicdepthbuffer.ts @@ -0,0 +1,245 @@ +import * as THREE from 'three'; + +import { FontLoader } from 'three/addons/loaders/FontLoader.js'; +import { TextGeometry } from 'three/addons/geometries/TextGeometry.js'; + +import Stats from 'three/addons/libs/stats.module.js'; + +// 1 micrometer to 100 billion light years in one scene, with 1 unit = 1 meter? preposterous! and yet... +const NEAR = 1e-6, + FAR = 1e27; +let SCREEN_WIDTH = window.innerWidth; +let SCREEN_HEIGHT = window.innerHeight; +let screensplit = 0.25, + screensplit_right = 0; +const mouse = [0.5, 0.5]; +let zoompos = -100, + minzoomspeed = 0.015; +let zoomspeed = minzoomspeed; + +let container, border, stats; +const objects = {}; + +// Generate a number of text labels, from 1µm in size up to 100,000,000 light years +// Try to use some descriptive real-world examples of objects at each scale + +const labeldata = [ + { size: 0.01, scale: 0.0001, label: 'microscopic (1µm)' }, // FIXME - triangulating text fails at this size, so we scale instead + { size: 0.01, scale: 0.1, label: 'minuscule (1mm)' }, + { size: 0.01, scale: 1.0, label: 'tiny (1cm)' }, + { size: 1, scale: 1.0, label: 'child-sized (1m)' }, + { size: 10, scale: 1.0, label: 'tree-sized (10m)' }, + { size: 100, scale: 1.0, label: 'building-sized (100m)' }, + { size: 1000, scale: 1.0, label: 'medium (1km)' }, + { size: 10000, scale: 1.0, label: 'city-sized (10km)' }, + { size: 3400000, scale: 1.0, label: 'moon-sized (3,400 Km)' }, + { size: 12000000, scale: 1.0, label: 'planet-sized (12,000 km)' }, + { size: 1400000000, scale: 1.0, label: 'sun-sized (1,400,000 km)' }, + { size: 7.47e12, scale: 1.0, label: 'solar system-sized (50Au)' }, + { size: 9.4605284e15, scale: 1.0, label: 'gargantuan (1 light year)' }, + { size: 3.08567758e16, scale: 1.0, label: 'ludicrous (1 parsec)' }, + { size: 1e19, scale: 1.0, label: 'mind boggling (1000 light years)' }, +]; + +init().then(animate); + +async function init() { + container = document.getElementById('container'); + + const loader = new FontLoader(); + const font = await loader.loadAsync('fonts/helvetiker_regular.typeface.json'); + + const scene = initScene(font); + + // Initialize two copies of the same scene, one with normal z-buffer and one with logarithmic z-buffer + objects.normal = await initView(scene, 'normal', false); + objects.logzbuf = await initView(scene, 'logzbuf', true); + + stats = new Stats(); + container.appendChild(stats.dom); + + // Resize border allows the user to easily compare effects of logarithmic depth buffer over the whole scene + border = document.getElementById('renderer_border'); + border.addEventListener('pointerdown', onBorderPointerDown); + + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('resize', onWindowResize); + window.addEventListener('wheel', onMouseWheel); +} + +async function initView(scene, name, logDepthBuf) { + const framecontainer = document.getElementById('container_' + name); + + const camera = new THREE.PerspectiveCamera(50, (screensplit * SCREEN_WIDTH) / SCREEN_HEIGHT, NEAR, FAR); + scene.add(camera); + + const renderer = new THREE.WebGPURenderer({ antialias: true, logarithmicDepthBuffer: logDepthBuf }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(SCREEN_WIDTH / 2, SCREEN_HEIGHT); + renderer.domElement.style.position = 'relative'; + renderer.domElement.id = 'renderer_' + name; + framecontainer.appendChild(renderer.domElement); + + await renderer.init(); + + return { container: framecontainer, renderer: renderer, scene: scene, camera: camera }; +} + +function initScene(font) { + const scene = new THREE.Scene(); + + scene.add(new THREE.AmbientLight(0x777777)); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(100, 100, 100); + scene.add(light); + + const materialargs = { + color: 0xffffff, + specular: 0x050505, + shininess: 50, + emissive: 0x000000, + }; + + const geometry = new THREE.SphereGeometry(0.5, 24, 12); + + for (let i = 0; i < labeldata.length; i++) { + const scale = labeldata[i].scale || 1; + + const labelgeo = new TextGeometry(labeldata[i].label, { + font: font, + size: labeldata[i].size, + depth: labeldata[i].size / 2, + }); + + labelgeo.computeBoundingSphere(); + + // center text + labelgeo.translate(-labelgeo.boundingSphere.radius, 0, 0); + + materialargs.color = new THREE.Color().setHSL(Math.random(), 0.5, 0.5); + + const material = new THREE.MeshPhongMaterial(materialargs); + + const group = new THREE.Group(); + group.position.z = -labeldata[i].size * scale; + scene.add(group); + + const textmesh = new THREE.Mesh(labelgeo, material); + textmesh.scale.set(scale, scale, scale); + textmesh.position.z = -labeldata[i].size * scale; + textmesh.position.y = (labeldata[i].size / 4) * scale; + group.add(textmesh); + + const dotmesh = new THREE.Mesh(geometry, material); + dotmesh.position.y = (-labeldata[i].size / 4) * scale; + dotmesh.scale.multiplyScalar(labeldata[i].size * scale); + group.add(dotmesh); + } + + return scene; +} + +function updateRendererSizes() { + // Recalculate size for both renderers when screen size or split location changes + + SCREEN_WIDTH = window.innerWidth; + SCREEN_HEIGHT = window.innerHeight; + + screensplit_right = 1 - screensplit; + + objects.normal.renderer.setSize(screensplit * SCREEN_WIDTH, SCREEN_HEIGHT); + objects.normal.camera.aspect = (screensplit * SCREEN_WIDTH) / SCREEN_HEIGHT; + objects.normal.camera.updateProjectionMatrix(); + objects.normal.camera.setViewOffset(SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, SCREEN_WIDTH * screensplit, SCREEN_HEIGHT); + objects.normal.container.style.width = screensplit * 100 + '%'; + + objects.logzbuf.renderer.setSize(screensplit_right * SCREEN_WIDTH, SCREEN_HEIGHT); + objects.logzbuf.camera.aspect = (screensplit_right * SCREEN_WIDTH) / SCREEN_HEIGHT; + objects.logzbuf.camera.updateProjectionMatrix(); + objects.logzbuf.camera.setViewOffset( + SCREEN_WIDTH, + SCREEN_HEIGHT, + SCREEN_WIDTH * screensplit, + 0, + SCREEN_WIDTH * screensplit_right, + SCREEN_HEIGHT, + ); + objects.logzbuf.container.style.width = screensplit_right * 100 + '%'; + + border.style.left = screensplit * 100 + '%'; +} + +function animate() { + requestAnimationFrame(animate); + + // Put some limits on zooming + const minzoom = labeldata[0].size * labeldata[0].scale * 1; + const maxzoom = labeldata[labeldata.length - 1].size * labeldata[labeldata.length - 1].scale * 100; + let damping = Math.abs(zoomspeed) > minzoomspeed ? 0.95 : 1.0; + + // Zoom out faster the further out you go + const zoom = THREE.MathUtils.clamp(Math.pow(Math.E, zoompos), minzoom, maxzoom); + zoompos = Math.log(zoom); + + // Slow down quickly at the zoom limits + if ((zoom == minzoom && zoomspeed < 0) || (zoom == maxzoom && zoomspeed > 0)) { + damping = 0.85; + } + + zoompos += zoomspeed; + zoomspeed *= damping; + + objects.normal.camera.position.x = Math.sin(0.5 * Math.PI * (mouse[0] - 0.5)) * zoom; + objects.normal.camera.position.y = Math.sin(0.25 * Math.PI * (mouse[1] - 0.5)) * zoom; + objects.normal.camera.position.z = Math.cos(0.5 * Math.PI * (mouse[0] - 0.5)) * zoom; + objects.normal.camera.lookAt(objects.normal.scene.position); + + // Clone camera settings across both scenes + objects.logzbuf.camera.position.copy(objects.normal.camera.position); + objects.logzbuf.camera.quaternion.copy(objects.normal.camera.quaternion); + + // Update renderer sizes if the split has changed + if (screensplit_right != 1 - screensplit) { + updateRendererSizes(); + } + + objects.normal.renderer.render(objects.normal.scene, objects.normal.camera); + objects.logzbuf.renderer.render(objects.logzbuf.scene, objects.logzbuf.camera); + + stats.update(); +} + +function onWindowResize() { + updateRendererSizes(); +} + +function onBorderPointerDown() { + // activate draggable window resizing bar + window.addEventListener('pointermove', onBorderPointerMove); + window.addEventListener('pointerup', onBorderPointerUp); +} + +function onBorderPointerMove(ev) { + screensplit = Math.max(0, Math.min(1, ev.clientX / window.innerWidth)); +} + +function onBorderPointerUp() { + window.removeEventListener('pointermove', onBorderPointerMove); + window.removeEventListener('pointerup', onBorderPointerUp); +} + +function onMouseMove(ev) { + mouse[0] = ev.clientX / window.innerWidth; + mouse[1] = ev.clientY / window.innerHeight; +} + +function onMouseWheel(ev) { + const amount = ev.deltaY; + if (amount === 0) return; + const dir = amount / Math.abs(amount); + zoomspeed = dir / 10; + + // Slow down default zoom speed after user starts zooming, to give them more control + minzoomspeed = 0.001; +} diff --git a/examples-testing/examples/webgpu_clearcoat.ts b/examples-testing/examples/webgpu_clearcoat.ts new file mode 100644 index 000000000..0d5b70a2f --- /dev/null +++ b/examples-testing/examples/webgpu_clearcoat.ts @@ -0,0 +1,205 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { HDRCubeTextureLoader } from 'three/addons/loaders/HDRCubeTextureLoader.js'; + +import { FlakesTexture } from 'three/addons/textures/FlakesTexture.js'; + +let container, stats; + +let camera, scene, renderer; + +let particleLight; +let group; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(27, window.innerWidth / window.innerHeight, 0.25, 50); + camera.position.z = 10; + + scene = new THREE.Scene(); + + group = new THREE.Group(); + scene.add(group); + + new HDRCubeTextureLoader() + .setPath('textures/cube/pisaHDR/') + .load(['px.hdr', 'nx.hdr', 'py.hdr', 'ny.hdr', 'pz.hdr', 'nz.hdr'], function (texture) { + const geometry = new THREE.SphereGeometry(0.8, 64, 32); + + const textureLoader = new THREE.TextureLoader(); + + const diffuse = textureLoader.load('textures/carbon/Carbon.png'); + diffuse.colorSpace = THREE.SRGBColorSpace; + diffuse.wrapS = THREE.RepeatWrapping; + diffuse.wrapT = THREE.RepeatWrapping; + diffuse.repeat.x = 10; + diffuse.repeat.y = 10; + + const normalMap = textureLoader.load('textures/carbon/Carbon_Normal.png'); + normalMap.wrapS = THREE.RepeatWrapping; + normalMap.wrapT = THREE.RepeatWrapping; + normalMap.repeat.x = 10; + normalMap.repeat.y = 10; + + const normalMap2 = textureLoader.load('textures/water/Water_1_M_Normal.jpg'); + + const normalMap3 = new THREE.CanvasTexture(new FlakesTexture()); + normalMap3.wrapS = THREE.RepeatWrapping; + normalMap3.wrapT = THREE.RepeatWrapping; + normalMap3.repeat.x = 10; + normalMap3.repeat.y = 6; + normalMap3.anisotropy = 16; + + const normalMap4 = textureLoader.load('textures/golfball.jpg'); + + const clearcoatNormalMap = textureLoader.load( + 'textures/pbr/Scratched_gold/Scratched_gold_01_1K_Normal.png', + ); + + // car paint + + let material = new THREE.MeshPhysicalMaterial({ + clearcoat: 1.0, + clearcoatRoughness: 0.1, + metalness: 0.9, + roughness: 0.5, + color: 0x0000ff, + normalMap: normalMap3, + normalScale: new THREE.Vector2(0.15, 0.15), + }); + let mesh = new THREE.Mesh(geometry, material); + mesh.position.x = -1; + mesh.position.y = 1; + group.add(mesh); + + // fibers + + material = new THREE.MeshPhysicalMaterial({ + roughness: 0.5, + clearcoat: 1.0, + clearcoatRoughness: 0.1, + map: diffuse, + normalMap: normalMap, + }); + mesh = new THREE.Mesh(geometry, material); + mesh.position.x = 1; + mesh.position.y = 1; + group.add(mesh); + + // golf + + material = new THREE.MeshPhysicalMaterial({ + metalness: 0.0, + roughness: 0.1, + clearcoat: 1.0, + normalMap: normalMap4, + clearcoatNormalMap: clearcoatNormalMap, + + // y scale is negated to compensate for normal map handedness. + clearcoatNormalScale: new THREE.Vector2(2.0, -2.0), + }); + mesh = new THREE.Mesh(geometry, material); + mesh.position.x = -1; + mesh.position.y = -1; + group.add(mesh); + + // clearcoat + normalmap + + material = new THREE.MeshPhysicalMaterial({ + clearcoat: 1.0, + metalness: 1.0, + color: 0xff0000, + normalMap: normalMap2, + normalScale: new THREE.Vector2(0.15, 0.15), + clearcoatNormalMap: clearcoatNormalMap, + + // y scale is negated to compensate for normal map handedness. + clearcoatNormalScale: new THREE.Vector2(2.0, -2.0), + }); + mesh = new THREE.Mesh(geometry, material); + mesh.position.x = 1; + mesh.position.y = -1; + group.add(mesh); + + // + + scene.background = texture; + scene.environment = texture; + }); + + // LIGHTS + + particleLight = new THREE.Mesh( + new THREE.SphereGeometry(0.05, 8, 8), + new THREE.MeshBasicMaterial({ color: 0xffffff }), + ); + scene.add(particleLight); + + particleLight.add(new THREE.PointLight(0xffffff, 30)); + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + // + + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1.25; + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // EVENTS + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 3; + controls.maxDistance = 30; + + window.addEventListener('resize', onWindowResize); +} + +// + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); +} + +// + +function animate() { + render(); + + stats.update(); +} + +function render() { + const timer = Date.now() * 0.00025; + + particleLight.position.x = Math.sin(timer * 7) * 3; + particleLight.position.y = Math.cos(timer * 5) * 4; + particleLight.position.z = Math.cos(timer * 3) * 3; + + for (let i = 0; i < group.children.length; i++) { + const child = group.children[i]; + child.rotation.y += 0.005; + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_clipping.ts b/examples-testing/examples/webgpu_clipping.ts new file mode 100644 index 000000000..e57a7e96c --- /dev/null +++ b/examples-testing/examples/webgpu_clipping.ts @@ -0,0 +1,207 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer, startTime, object, stats; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(36, window.innerWidth / window.innerHeight, 0.25, 16); + + camera.position.set(0, 1.3, 3); + + scene = new THREE.Scene(); + + // Lights + + scene.add(new THREE.AmbientLight(0xcccccc)); + + const spotLight = new THREE.SpotLight(0xffffff, 60); + spotLight.angle = Math.PI / 5; + spotLight.penumbra = 0.2; + spotLight.position.set(2, 3, 3); + spotLight.castShadow = true; + spotLight.shadow.camera.near = 3; + spotLight.shadow.camera.far = 10; + spotLight.shadow.mapSize.width = 2048; + spotLight.shadow.mapSize.height = 2048; + spotLight.shadow.bias = -0.002; + spotLight.shadow.radius = 4; + scene.add(spotLight); + + const dirLight = new THREE.DirectionalLight(0x55505a, 3); + dirLight.position.set(0, 3, 0); + dirLight.castShadow = true; + dirLight.shadow.camera.near = 1; + dirLight.shadow.camera.far = 10; + + dirLight.shadow.camera.right = 1; + dirLight.shadow.camera.left = -1; + dirLight.shadow.camera.top = 1; + dirLight.shadow.camera.bottom = -1; + + dirLight.shadow.mapSize.width = 1024; + dirLight.shadow.mapSize.height = 1024; + scene.add(dirLight); + + // ***** Clipping planes: ***** + + const localPlane = new THREE.Plane(new THREE.Vector3(0, -1, 0), 0.8); + const localPlane2 = new THREE.Plane(new THREE.Vector3(0, 0, -1), 0.1); + const globalPlane = new THREE.Plane(new THREE.Vector3(-1, 0, 0), 0.1); + + // Geometry + + const material = new THREE.MeshPhongNodeMaterial({ + color: 0x80ee10, + shininess: 0, + side: THREE.DoubleSide, + + // ***** Clipping setup (material): ***** + clippingPlanes: [localPlane, localPlane2], + clipShadows: true, + alphaToCoverage: true, + clipIntersection: true, + }); + + const geometry = new THREE.TorusKnotGeometry(0.4, 0.08, 95, 20); + + object = new THREE.Mesh(geometry, material); + object.castShadow = true; + scene.add(object); + + const ground = new THREE.Mesh( + new THREE.PlaneGeometry(9, 9, 1, 1), + new THREE.MeshPhongNodeMaterial({ color: 0xa0adaf, shininess: 150 }), + ); + + ground.rotation.x = -Math.PI / 2; // rotates X/Y to X/Z + ground.receiveShadow = true; + scene.add(ground); + + // Stats + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // Renderer + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.shadowMap.enabled = true; + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + window.addEventListener('resize', onWindowResize); + document.body.appendChild(renderer.domElement); + + // ***** Clipping setup (renderer): ***** + const globalPlanes = [globalPlane]; + const Empty = Object.freeze([]); + + renderer.clippingPlanes = Empty; // GUI sets it to globalPlanes + renderer.localClippingEnabled = true; + + // Controls + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 1, 0); + controls.update(); + + // GUI + + const gui = new GUI(), + props = { + alphaToCoverage: true, + }, + folderLocal = gui.addFolder('Local Clipping'), + propsLocal = { + get Enabled() { + return renderer.localClippingEnabled; + }, + set Enabled(v) { + renderer.localClippingEnabled = v; + }, + + get Shadows() { + return material.clipShadows; + }, + set Shadows(v) { + material.clipShadows = v; + }, + + get Intersection() { + return material.clipIntersection; + }, + + set Intersection(v) { + material.clipIntersection = v; + }, + + get Plane() { + return localPlane.constant; + }, + set Plane(v) { + localPlane.constant = v; + }, + }, + folderGlobal = gui.addFolder('Global Clipping'), + propsGlobal = { + get Enabled() { + return renderer.clippingPlanes !== Empty; + }, + set Enabled(v) { + renderer.clippingPlanes = v ? globalPlanes : Empty; + }, + + get Plane() { + return globalPlane.constant; + }, + set Plane(v) { + globalPlane.constant = v; + }, + }; + + gui.add(props, 'alphaToCoverage').onChange(function (value) { + ground.material.alphaToCoverage = value; + ground.material.needsUpdate = true; + + material.alphaToCoverage = value; + material.needsUpdate = true; + }); + + folderLocal.add(propsLocal, 'Enabled'); + folderLocal.add(propsLocal, 'Shadows'); + folderLocal.add(propsLocal, 'Intersection'); + folderLocal.add(propsLocal, 'Plane', 0.3, 1.25); + + folderGlobal.add(propsGlobal, 'Enabled'); + folderGlobal.add(propsGlobal, 'Plane', -0.4, 3); + + // Start + + startTime = Date.now(); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate(currentTime) { + const time = (currentTime - startTime) / 1000; + + object.position.y = 0.8; + object.rotation.x = time * 0.5; + object.rotation.y = time * 0.2; + object.scale.setScalar(Math.cos(time) * 0.125 + 0.875); + + stats.begin(); + renderer.render(scene, camera); + stats.end(); +} diff --git a/examples-testing/examples/webgpu_custom_fog_background.ts b/examples-testing/examples/webgpu_custom_fog_background.ts new file mode 100644 index 000000000..4a2e6c800 --- /dev/null +++ b/examples-testing/examples/webgpu_custom_fog_background.ts @@ -0,0 +1,93 @@ +import * as THREE from 'three'; +import { pass, color, rangeFog } from 'three/tsl'; + +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +let camera, scene, renderer; +let postProcessing; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 20); + camera.position.set(-1.8, 0.6, 2.7); + + scene = new THREE.Scene(); + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + //renderer.toneMapping = THREE.ACESFilmicToneMapping; // apply tone mapping in post processing + container.appendChild(renderer.domElement); + + // post processing + + // render scene pass ( the same of css ) + const scenePass = pass(scene, camera); + const scenePassViewZ = scenePass.getViewZNode(); + + // background color + const backgroundColor = color(0x0066ff); + + // get fog factor from scene pass context + // equivalent to: scene.fog = new THREE.Fog( 0x0066ff, 2.7, 4 ); + const fogFactor = rangeFog(null, 2.7, 4).context({ getViewZ: () => scenePassViewZ }); + + // tone mapping scene pass + const scenePassTM = scenePass.toneMapping(THREE.ACESFilmicToneMapping); + + // mix fog from fog factor and background color + const compose = fogFactor.mix(scenePassTM, backgroundColor); + + postProcessing = new THREE.PostProcessing(renderer); + postProcessing.outputNode = compose; + + // + + new RGBELoader().setPath('textures/equirectangular/').load('royal_esplanade_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.environment = texture; + + // model + + const loader = new GLTFLoader().setPath('models/gltf/DamagedHelmet/glTF/'); + loader.load('DamagedHelmet.gltf', function (gltf) { + scene.add(gltf.scene); + + render(); + }); + }); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 2; + controls.maxDistance = 5; + controls.target.set(0, -0.1, -0.2); + controls.update(); + controls.addEventListener('change', render); // use if there is no animation loop + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +// + +function render() { + postProcessing.renderAsync(); +} diff --git a/examples-testing/examples/webgpu_display_stereo.ts b/examples-testing/examples/webgpu_display_stereo.ts new file mode 100644 index 000000000..48358dcf8 --- /dev/null +++ b/examples-testing/examples/webgpu_display_stereo.ts @@ -0,0 +1,138 @@ +import * as THREE from 'three'; + +import { stereoPass, anaglyphPass, parallaxBarrierPass } from 'three/tsl'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { Timer } from 'three/addons/misc/Timer.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, postProcessing; + +let stereo, anaglyph, parallaxBarrier; + +let mesh, dummy, timer; + +const position = new THREE.Vector3(); + +const params = { + effect: 'stereo', +}; + +const effects = { Stereo: 'stereo', Anaglyph: 'anaglyph', ParallaxBarrier: 'parallaxBarrier' }; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.z = 3; + + scene = new THREE.Scene(); + scene.background = new THREE.CubeTextureLoader() + .setPath('textures/cube/Park3Med/') + .load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']); + + timer = new Timer(); + + const geometry = new THREE.SphereGeometry(0.1, 32, 16); + + const textureCube = new THREE.CubeTextureLoader() + .setPath('textures/cube/Park3Med/') + .load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']); + + const material = new THREE.MeshBasicMaterial({ color: 0xffffff, envMap: textureCube }); + + mesh = new THREE.InstancedMesh(geometry, material, 500); + mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage); + + dummy = new THREE.Mesh(); + + for (let i = 0; i < 500; i++) { + dummy.position.x = Math.random() * 10 - 5; + dummy.position.y = Math.random() * 10 - 5; + dummy.position.z = Math.random() * 10 - 5; + dummy.scale.x = dummy.scale.y = dummy.scale.z = Math.random() * 3 + 1; + + dummy.updateMatrix(); + + mesh.setMatrixAt(i, dummy.matrix); + } + + scene.add(mesh); + + // + + renderer = new THREE.WebGPURenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + postProcessing = new THREE.PostProcessing(renderer); + stereo = stereoPass(scene, camera); + anaglyph = anaglyphPass(scene, camera); + parallaxBarrier = parallaxBarrierPass(scene, camera); + + postProcessing.outputNode = stereo; + + // + + const gui = new GUI(); + gui.add(params, 'effect', effects).onChange(update); + + // + + window.addEventListener('resize', onWindowResize); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 1; + controls.maxDistance = 25; +} + +function update(value) { + if (value === 'stereo') { + postProcessing.outputNode = stereo; + } else if (value === 'anaglyph') { + postProcessing.outputNode = anaglyph; + } else if (value === 'parallaxBarrier') { + postProcessing.outputNode = parallaxBarrier; + } + + postProcessing.needsUpdate = true; +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function extractPosition(matrix, position) { + position.x = matrix.elements[12]; + position.y = matrix.elements[13]; + position.z = matrix.elements[14]; +} + +// + +function animate() { + timer.update(); + + const elapsedTime = timer.getElapsed() * 0.1; + + for (let i = 0; i < mesh.count; i++) { + mesh.getMatrixAt(i, dummy.matrix); + + extractPosition(dummy.matrix, position); + + position.x = 5 * Math.cos(elapsedTime + i); + position.y = 5 * Math.sin(elapsedTime + i * 1.1); + + dummy.matrix.setPosition(position); + + mesh.setMatrixAt(i, dummy.matrix); + + mesh.instanceMatrix.needsUpdate = true; + } + + postProcessing.render(); +} diff --git a/examples-testing/examples/webgpu_instancing_morph.ts b/examples-testing/examples/webgpu_instancing_morph.ts new file mode 100644 index 000000000..cfd721721 --- /dev/null +++ b/examples-testing/examples/webgpu_instancing_morph.ts @@ -0,0 +1,148 @@ +import * as THREE from 'three'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let camera, scene, renderer, stats, mesh, mixer, dummy; + +const offset = 5000; + +const timeOffsets = new Float32Array(1024); + +for (let i = 0; i < 1024; i++) { + timeOffsets[i] = Math.random() * 3; +} + +const clock = new THREE.Clock(true); + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 100, 10000); + + scene = new THREE.Scene(); + + scene.background = new THREE.Color(0x99ddff); + + scene.fog = new THREE.Fog(0x99ddff, 5000, 10000); + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + const light = new THREE.DirectionalLight(0xffffff, 1); + + light.position.set(200, 1000, 50); + + light.shadow.mapSize.width = 2048; + light.shadow.mapSize.height = 2048; + light.castShadow = true; + + light.shadow.camera.left = -5000; + light.shadow.camera.right = 5000; + light.shadow.camera.top = 5000; + light.shadow.camera.bottom = -5000; + light.shadow.camera.far = 2000; + + light.shadow.bias = -0.01; + + light.shadow.camera.updateProjectionMatrix(); + + scene.add(light); + + const hemi = new THREE.HemisphereLight(0x99ddff, 0x669933, 1 / 3); + + scene.add(hemi); + + const ground = new THREE.Mesh( + new THREE.PlaneGeometry(1000000, 1000000), + new THREE.MeshStandardMaterial({ color: 0x669933 }), + ); + + ground.rotation.x = -Math.PI / 2; + + ground.receiveShadow = true; + + scene.add(ground); + + const loader = new GLTFLoader(); + + loader.load('models/gltf/Horse.glb', function (glb) { + dummy = glb.scene.children[0]; + + mesh = new THREE.InstancedMesh( + dummy.geometry, + new THREE.MeshStandardNodeMaterial({ + flatShading: true, + }), + 1024, + ); + + mesh.castShadow = true; + + for (let x = 0, i = 0; x < 32; x++) { + for (let y = 0; y < 32; y++) { + dummy.position.set(offset - 300 * x + 200 * Math.random(), 0, offset - 300 * y); + + dummy.updateMatrix(); + + mesh.setMatrixAt(i, dummy.matrix); + + mesh.setColorAt(i, new THREE.Color(`hsl(${Math.random() * 360}, 50%, 66%)`)); + + i++; + } + } + + scene.add(mesh); + + mixer = new THREE.AnimationMixer(glb.scene); + + const action = mixer.clipAction(glb.animations[0]); + + action.play(); + }); + + // renderer + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setAnimationLoop(animate); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const time = clock.getElapsedTime(); + + const r = 3000; + camera.position.set(Math.sin(time / 10) * r, 1500 + 1000 * Math.cos(time / 5), Math.cos(time / 10) * r); + camera.lookAt(0, 0, 0); + + if (mesh) { + for (let i = 0; i < 1024; i++) { + mixer.setTime(time + timeOffsets[i]); + + mesh.setMorphAt(i, dummy); + } + + mesh.morphTexture.needsUpdate = true; + } + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgpu_lightprobe.ts b/examples-testing/examples/webgpu_lightprobe.ts new file mode 100644 index 000000000..a34ad2dec --- /dev/null +++ b/examples-testing/examples/webgpu_lightprobe.ts @@ -0,0 +1,126 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { LightProbeGenerator } from 'three/addons/lights/LightProbeGenerator.js'; + +let mesh, renderer, scene, camera; + +let gui; + +let lightProbe; +let directionalLight; + +// linear color space +const API = { + lightProbeIntensity: 1.0, + directionalLightIntensity: 0.6, + envMapIntensity: 1, +}; + +init(); + +function init() { + // renderer + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // tone mapping + renderer.toneMapping = THREE.NoToneMapping; + + // scene + scene = new THREE.Scene(); + + // camera + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 0, 30); + + // controls + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 10; + controls.maxDistance = 50; + controls.enablePan = false; + + // probe + lightProbe = new THREE.LightProbe(); + scene.add(lightProbe); + + // light + directionalLight = new THREE.DirectionalLight(0xffffff, API.directionalLightIntensity); + directionalLight.position.set(10, 10, 10); + scene.add(directionalLight); + + // envmap + const genCubeUrls = function (prefix, postfix) { + return [ + prefix + 'px' + postfix, + prefix + 'nx' + postfix, + prefix + 'py' + postfix, + prefix + 'ny' + postfix, + prefix + 'pz' + postfix, + prefix + 'nz' + postfix, + ]; + }; + + const urls = genCubeUrls('textures/cube/pisa/', '.png'); + + new THREE.CubeTextureLoader().load(urls, function (cubeTexture) { + scene.background = cubeTexture; + + lightProbe.copy(LightProbeGenerator.fromCubeTexture(cubeTexture)); + + const geometry = new THREE.SphereGeometry(5, 64, 32); + //const geometry = new THREE.TorusKnotGeometry( 4, 1.5, 256, 32, 2, 3 ); + + const material = new THREE.MeshStandardMaterial({ + color: 0xffffff, + metalness: 0, + roughness: 0, + envMap: cubeTexture, + envMapIntensity: API.envMapIntensity, + }); + + // mesh + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + }); + + // gui + gui = new GUI({ title: 'Intensity' }); + + gui.add(API, 'lightProbeIntensity', 0, 1, 0.02) + .name('light probe') + .onChange(function () { + lightProbe.intensity = API.lightProbeIntensity; + }); + + gui.add(API, 'directionalLightIntensity', 0, 1, 0.02) + .name('directional light') + .onChange(function () { + directionalLight.intensity = API.directionalLightIntensity; + }); + + gui.add(API, 'envMapIntensity', 0, 1, 0.02) + .name('envMap') + .onChange(function () { + mesh.material.envMapIntensity = API.envMapIntensity; + }); + + // listener + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); +} + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_lights_ies_spotlight.ts b/examples-testing/examples/webgpu_lights_ies_spotlight.ts new file mode 100644 index 000000000..41b56de88 --- /dev/null +++ b/examples-testing/examples/webgpu_lights_ies_spotlight.ts @@ -0,0 +1,117 @@ +import * as THREE from 'three'; + +import { OrbitControls } from './jsm/controls/OrbitControls.js'; + +import { IESLoader } from 'three/addons/loaders/IESLoader.js'; + +let renderer, scene, camera; +let lights; + +async function init() { + const iesLoader = new IESLoader().setPath('./ies/'); + //iesLoader.type = THREE.UnsignedByteType; // LDR + + const [iesTexture1, iesTexture2, iesTexture3, iesTexture4] = await Promise.all([ + iesLoader.loadAsync('007cfb11e343e2f42e3b476be4ab684e.ies'), + iesLoader.loadAsync('06b4cfdc8805709e767b5e2e904be8ad.ies'), + iesLoader.loadAsync('02a7562c650498ebb301153dbbf59207.ies'), + iesLoader.loadAsync('1a936937a49c63374e6d4fbed9252b29.ies'), + ]); + + // + + scene = new THREE.Scene(); + + // + + const spotLight = new THREE.IESSpotLight(0xff0000, 500); + spotLight.position.set(6.5, 1.5, 6.5); + spotLight.angle = Math.PI / 8; + spotLight.penumbra = 0.7; + spotLight.distance = 20; + spotLight.iesMap = iesTexture1; + scene.add(spotLight); + + // + + const spotLight2 = new THREE.IESSpotLight(0x00ff00, 500); + spotLight2.position.set(6.5, 1.5, -6.5); + spotLight2.angle = Math.PI / 8; + spotLight2.penumbra = 0.7; + spotLight2.distance = 20; + spotLight2.iesMap = iesTexture2; + scene.add(spotLight2); + + // + + const spotLight3 = new THREE.IESSpotLight(0x0000ff, 500); + spotLight3.position.set(-6.5, 1.5, -6.5); + spotLight3.angle = Math.PI / 8; + spotLight3.penumbra = 0.7; + spotLight3.distance = 20; + spotLight3.iesMap = iesTexture3; + scene.add(spotLight3); + + // + + const spotLight4 = new THREE.IESSpotLight(0xffffff, 500); + spotLight4.position.set(-6.5, 1.5, 6.5); + spotLight4.angle = Math.PI / 8; + spotLight4.penumbra = 0.7; + spotLight4.distance = 20; + spotLight4.iesMap = iesTexture4; + scene.add(spotLight4); + + // + + lights = [spotLight, spotLight2, spotLight3, spotLight4]; + + // + + const material = new THREE.MeshPhongMaterial({ color: 0x808080 /*, dithering: true*/ }); + + const geometry = new THREE.PlaneGeometry(200, 200); + + const mesh = new THREE.Mesh(geometry, material); + mesh.rotation.x = -Math.PI * 0.5; + scene.add(mesh); + + // + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(render); + document.body.appendChild(renderer.domElement); + + camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(16, 4, 1); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 2; + controls.maxDistance = 50; + controls.enablePan = false; + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function render(time) { + time = (time / 1000) * 2.0; + + for (let i = 0; i < lights.length; i++) { + lights[i].position.y = Math.sin(time + i) + 0.97; + } + + renderer.render(scene, camera); +} + +init(); diff --git a/examples-testing/examples/webgpu_lights_rectarealight.ts b/examples-testing/examples/webgpu_lights_rectarealight.ts new file mode 100644 index 000000000..5638c9029 --- /dev/null +++ b/examples-testing/examples/webgpu_lights_rectarealight.ts @@ -0,0 +1,79 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { RectAreaLightHelper } from 'three/addons/helpers/RectAreaLightHelper.js'; +import { RectAreaLightTexturesLib } from 'three/addons/lights/RectAreaLightTexturesLib.js'; + +let renderer, scene, camera; +let stats, meshKnot; + +init(); + +function init() { + THREE.RectAreaLightNode.setLTC(RectAreaLightTexturesLib.init()); + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animation); + document.body.appendChild(renderer.domElement); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.set(0, 5, -15); + + scene = new THREE.Scene(); + + const rectLight1 = new THREE.RectAreaLight(0xff0000, 5, 4, 10); + rectLight1.position.set(-5, 5, 5); + scene.add(rectLight1); + + const rectLight2 = new THREE.RectAreaLight(0x00ff00, 5, 4, 10); + rectLight2.position.set(0, 5, 5); + scene.add(rectLight2); + + const rectLight3 = new THREE.RectAreaLight(0x0000ff, 5, 4, 10); + rectLight3.position.set(5, 5, 5); + scene.add(rectLight3); + + scene.add(new RectAreaLightHelper(rectLight1)); + scene.add(new RectAreaLightHelper(rectLight2)); + scene.add(new RectAreaLightHelper(rectLight3)); + + const geoFloor = new THREE.BoxGeometry(2000, 0.1, 2000); + const matStdFloor = new THREE.MeshStandardMaterial({ color: 0xbcbcbc, roughness: 0.1, metalness: 0 }); + const mshStdFloor = new THREE.Mesh(geoFloor, matStdFloor); + scene.add(mshStdFloor); + + const geoKnot = new THREE.TorusKnotGeometry(1.5, 0.5, 200, 16); + const matKnot = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0, metalness: 0 }); + meshKnot = new THREE.Mesh(geoKnot, matKnot); + meshKnot.position.set(0, 5, 0); + scene.add(meshKnot); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.target.copy(meshKnot.position); + controls.update(); + + // + + window.addEventListener('resize', onWindowResize); + + stats = new Stats(); + document.body.appendChild(stats.dom); +} + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); +} + +function animation(time) { + meshKnot.rotation.y = time / 1000; + + renderer.render(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgpu_loader_gltf.ts b/examples-testing/examples/webgpu_loader_gltf.ts new file mode 100644 index 000000000..64d1fda4b --- /dev/null +++ b/examples-testing/examples/webgpu_loader_gltf.ts @@ -0,0 +1,71 @@ +import * as THREE from 'three'; + +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +let camera, scene, renderer; + +init(); +render(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 20); + camera.position.set(-1.8, 0.6, 2.7); + + scene = new THREE.Scene(); + + new RGBELoader().setPath('textures/equirectangular/').load('royal_esplanade_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + //texture.minFilter = THREE.LinearMipmapLinearFilter; + //texture.generateMipmaps = true; + + scene.background = texture; + scene.environment = texture; + + render(); + + // model + + const loader = new GLTFLoader().setPath('models/gltf/DamagedHelmet/glTF/'); + loader.load('DamagedHelmet.gltf', function (gltf) { + scene.add(gltf.scene); + + render(); + }); + }); + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + container.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.addEventListener('change', render); // use if there is no animation loop + controls.minDistance = 2; + controls.maxDistance = 10; + controls.target.set(0, 0, -0.2); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +// + +function render() { + renderer.renderAsync(scene, camera); +} diff --git a/examples-testing/examples/webgpu_loader_gltf_anisotropy.ts b/examples-testing/examples/webgpu_loader_gltf_anisotropy.ts new file mode 100644 index 000000000..d100e8c81 --- /dev/null +++ b/examples-testing/examples/webgpu_loader_gltf_anisotropy.ts @@ -0,0 +1,65 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +let renderer, scene, camera, controls; + +init(); + +async function init() { + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(render); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1.35; + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.01, 10); + camera.position.set(-0.35, -0.2, 0.35); + + controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, -0.08, 0.11); + controls.minDistance = 0.1; + controls.maxDistance = 2; + controls.addEventListener('change', render); + controls.update(); + + const rgbeLoader = new RGBELoader().setPath('textures/equirectangular/'); + const gltfLoader = new GLTFLoader().setPath('models/gltf/'); + + const [texture, gltf] = await Promise.all([ + rgbeLoader.loadAsync('royal_esplanade_1k.hdr'), + gltfLoader.loadAsync('AnisotropyBarnLamp.glb'), + ]); + + // environment + + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + scene.backgroundBlurriness = 0.5; + scene.environment = texture; + + // model + + scene.add(gltf.scene); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function render() { + renderer.renderAsync(scene, camera); +} diff --git a/examples-testing/examples/webgpu_loader_gltf_compressed.ts b/examples-testing/examples/webgpu_loader_gltf_compressed.ts new file mode 100644 index 000000000..9405b64ae --- /dev/null +++ b/examples-testing/examples/webgpu_loader_gltf_compressed.ts @@ -0,0 +1,67 @@ +import * as THREE from 'three'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'; +import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer; + +init(); + +async function init() { + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 20); + camera.position.set(2, 2, 2); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xeeeeee); + + //lights + + const light = new THREE.PointLight(0xffffff); + light.power = 1300; + camera.add(light); + scene.add(camera); + + //renderer + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ReinhardToneMapping; + renderer.toneMappingExposure = 1; + document.body.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 3; + controls.maxDistance = 6; + controls.update(); + + const ktx2Loader = await new KTX2Loader().setTranscoderPath('jsm/libs/basis/').detectSupportAsync(renderer); + + const loader = new GLTFLoader(); + loader.setKTX2Loader(ktx2Loader); + loader.setMeshoptDecoder(MeshoptDecoder); + loader.load('models/gltf/coffeemat.glb', function (gltf) { + const gltfScene = gltf.scene; + gltfScene.position.y = -0.8; + gltfScene.scale.setScalar(0.01); + + scene.add(gltfScene); + }); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_loader_gltf_dispersion.ts b/examples-testing/examples/webgpu_loader_gltf_dispersion.ts new file mode 100644 index 000000000..c1f1ecc8f --- /dev/null +++ b/examples-testing/examples/webgpu_loader_gltf_dispersion.ts @@ -0,0 +1,63 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +let camera, scene, renderer; + +init().then(render); + +async function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 5); + camera.position.set(0.1, 0.05, 0.15); + + scene = new THREE.Scene(); + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(render); + renderer.toneMapping = THREE.ReinhardToneMapping; // TODO: Add THREE.NeutralToneMapping; + renderer.toneMappingExposure = 1; + container.appendChild(renderer.domElement); + + const rgbeLoader = await new RGBELoader() + .setPath('textures/equirectangular/') + .loadAsync('pedestrian_overpass_1k.hdr'); + rgbeLoader.mapping = THREE.EquirectangularReflectionMapping; + + scene = new THREE.Scene(); + scene.backgroundBlurriness = 0.5; + scene.environment = rgbeLoader; + scene.background = rgbeLoader; + + const loader = new GLTFLoader(); + const gltf = await loader.loadAsync('models/gltf/DispersionTest.glb'); + + scene.add(gltf.scene); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 0.1; + controls.maxDistance = 10; + controls.target.set(0, 0, 0); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_loader_gltf_iridescence.ts b/examples-testing/examples/webgpu_loader_gltf_iridescence.ts new file mode 100644 index 000000000..f163ea770 --- /dev/null +++ b/examples-testing/examples/webgpu_loader_gltf_iridescence.ts @@ -0,0 +1,70 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +let renderer, scene, camera, controls; + +init().catch(function (err) { + console.error(err); +}); + +async function init() { + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setAnimationLoop(render); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.05, 20); + camera.position.set(0.35, 0.05, 0.35); + + controls = new OrbitControls(camera, renderer.domElement); + controls.autoRotate = true; + controls.autoRotateSpeed = -0.5; + controls.target.set(0, 0.2, 0); + controls.update(); + + const rgbeLoader = new RGBELoader().setPath('textures/equirectangular/'); + + const gltfLoader = new GLTFLoader().setPath('models/gltf/'); + + const [texture, gltf] = await Promise.all([ + rgbeLoader.loadAsync('venice_sunset_1k.hdr'), + gltfLoader.loadAsync('IridescenceLamp.glb'), + ]); + + // environment + + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + scene.environment = texture; + + // model + + scene.add(gltf.scene); + + render(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + render(); +} + +function render() { + controls.update(); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_loader_gltf_sheen.ts b/examples-testing/examples/webgpu_loader_gltf_sheen.ts new file mode 100644 index 000000000..788ef2a89 --- /dev/null +++ b/examples-testing/examples/webgpu_loader_gltf_sheen.ts @@ -0,0 +1,81 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, controls; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 20); + camera.position.set(-0.75, 0.7, 1.25); + + scene = new THREE.Scene(); + //scene.add( new THREE.DirectionalLight( 0xffffff, 2 ) ); + + // model + + new GLTFLoader().setPath('models/gltf/').load('SheenChair.glb', function (gltf) { + scene.add(gltf.scene); + + const object = gltf.scene.getObjectByName('SheenChair_fabric'); + + const gui = new GUI(); + + gui.add(object.material, 'sheen', 0, 1); + gui.open(); + }); + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1; + container.appendChild(renderer.domElement); + + scene.background = new THREE.Color(0xaaaaaa); + + new RGBELoader().setPath('textures/equirectangular/').load('royal_esplanade_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + //scene.backgroundBlurriness = 1; // @TODO: Needs PMREM + scene.environment = texture; + }); + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.minDistance = 1; + controls.maxDistance = 10; + controls.target.set(0, 0.35, 0); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + controls.update(); // required if damping enabled + + render(); +} + +function render() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_loader_gltf_transmission.ts b/examples-testing/examples/webgpu_loader_gltf_transmission.ts new file mode 100644 index 000000000..040233262 --- /dev/null +++ b/examples-testing/examples/webgpu_loader_gltf_transmission.ts @@ -0,0 +1,80 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; + +let camera, scene, renderer, controls, clock, mixer; + +init(); + +function init() { + clock = new THREE.Clock(); + + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 20); + camera.position.set(0, 0.4, 0.7); + + scene = new THREE.Scene(); + + new RGBELoader().setPath('textures/equirectangular/').load('royal_esplanade_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + scene.backgroundBlurriness = 0.35; + + scene.environment = texture; + + // model + + new GLTFLoader() + .setPath('models/gltf/') + .setDRACOLoader(new DRACOLoader().setDecoderPath('jsm/libs/draco/gltf/')) + .load('IridescentDishWithOlives.glb', function (gltf) { + mixer = new THREE.AnimationMixer(gltf.scene); + mixer.clipAction(gltf.animations[0]).play(); + + scene.add(gltf.scene); + }); + }); + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setAnimationLoop(render); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1; + container.appendChild(renderer.domElement); + + controls = new OrbitControls(camera, renderer.domElement); + controls.autoRotate = true; + controls.autoRotateSpeed = -0.75; + controls.enableDamping = true; + controls.minDistance = 0.5; + controls.maxDistance = 1; + controls.target.set(0, 0.1, 0); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function render() { + if (mixer) mixer.update(clock.getDelta()); + + controls.update(); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_materials_basic.ts b/examples-testing/examples/webgpu_materials_basic.ts new file mode 100644 index 000000000..0161a9c7b --- /dev/null +++ b/examples-testing/examples/webgpu_materials_basic.ts @@ -0,0 +1,137 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer; + +const spheres = []; + +let mouseX = 0; +let mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +const params = { + color: '#ffffff', + mapping: THREE.CubeReflectionMapping, + refractionRatio: 0.98, + transparent: false, + opacity: 1, +}; + +const mappings = { ReflectionMapping: THREE.CubeReflectionMapping, RefractionMapping: THREE.CubeRefractionMapping }; + +document.addEventListener('mousemove', onDocumentMouseMove); + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.01, 100); + camera.position.z = 3; + + const path = './textures/cube/pisa/'; + const format = '.png'; + const urls = [ + path + 'px' + format, + path + 'nx' + format, + path + 'py' + format, + path + 'ny' + format, + path + 'pz' + format, + path + 'nz' + format, + ]; + + const textureCube = new THREE.CubeTextureLoader().load(urls); + + scene = new THREE.Scene(); + scene.background = textureCube; + + const geometry = new THREE.SphereGeometry(0.1, 32, 16); + const material = new THREE.MeshBasicMaterial({ color: 0xffffff, envMap: textureCube }); + + for (let i = 0; i < 500; i++) { + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.x = Math.random() * 10 - 5; + mesh.position.y = Math.random() * 10 - 5; + mesh.position.z = Math.random() * 10 - 5; + + mesh.scale.x = mesh.scale.y = mesh.scale.z = Math.random() * 3 + 1; + + scene.add(mesh); + + spheres.push(mesh); + } + + // + + renderer = new THREE.WebGPURenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + const gui = new GUI({ width: 300 }); + + gui.addColor(params, 'color').onChange(value => material.color.set(value)); + gui.add(params, 'mapping', mappings).onChange(value => { + textureCube.mapping = value; + material.needsUpdate = true; + }); + gui.add(params, 'refractionRatio') + .min(0.0) + .max(1.0) + .step(0.01) + .onChange(value => (material.refractionRatio = value)); + gui.add(params, 'transparent').onChange(value => { + material.transparent = value; + material.needsUpdate = true; + }); + gui.add(params, 'opacity') + .min(0.0) + .max(1.0) + .step(0.01) + .onChange(value => (material.opacity = value)); + gui.open(); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onDocumentMouseMove(event) { + mouseX = (event.clientX - windowHalfX) / 100; + mouseY = (event.clientY - windowHalfY) / 100; +} + +// + +function animate() { + const timer = 0.0001 * Date.now(); + + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-mouseY - camera.position.y) * 0.05; + + camera.lookAt(scene.position); + + for (let i = 0, il = spheres.length; i < il; i++) { + const sphere = spheres[i]; + + sphere.position.x = 5 * Math.cos(timer + i); + sphere.position.y = 5 * Math.sin(timer + i * 1.1); + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_materials_displacementmap.ts b/examples-testing/examples/webgpu_materials_displacementmap.ts new file mode 100644 index 000000000..54d26d65e --- /dev/null +++ b/examples-testing/examples/webgpu_materials_displacementmap.ts @@ -0,0 +1,224 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'; + +let stats; +let camera, scene, renderer, controls; + +const settings = { + metalness: 1.0, + roughness: 0.4, + ambientIntensity: 0.2, + aoMapIntensity: 1.0, + envMapIntensity: 1.0, + displacementScale: 2.436143, // from original model + normalScale: 1.0, +}; + +let mesh, material; + +let pointLight, ambientLight; + +const height = 500; // of camera frustum + +let r = 0.0; + +init(); +initGui(); + +// Init gui +function initGui() { + const gui = new GUI(); + + gui.add(settings, 'metalness') + .min(0) + .max(1) + .onChange(function (value) { + material.metalness = value; + }); + + gui.add(settings, 'roughness') + .min(0) + .max(1) + .onChange(function (value) { + material.roughness = value; + }); + + gui.add(settings, 'aoMapIntensity') + .min(0) + .max(1) + .onChange(function (value) { + material.aoMapIntensity = value; + }); + + gui.add(settings, 'ambientIntensity') + .min(0) + .max(1) + .onChange(function (value) { + ambientLight.intensity = value; + }); + + gui.add(settings, 'envMapIntensity') + .min(0) + .max(3) + .onChange(function (value) { + material.envMapIntensity = value; + }); + + gui.add(settings, 'displacementScale') + .min(0) + .max(3.0) + .onChange(function (value) { + material.displacementScale = value; + }); + + gui.add(settings, 'normalScale') + .min(-1) + .max(1) + .onChange(function (value) { + material.normalScale.set(1, -1).multiplyScalar(value); + }); +} + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + renderer = new THREE.WebGPURenderer(); + renderer.setAnimationLoop(animate); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + container.appendChild(renderer.domElement); + + // + + scene = new THREE.Scene(); + + const aspect = window.innerWidth / window.innerHeight; + camera = new THREE.OrthographicCamera(-height * aspect, height * aspect, height, -height, 1, 10000); + camera.position.z = 1500; + scene.add(camera); + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableZoom = false; + controls.enableDamping = true; + + // lights + + ambientLight = new THREE.AmbientLight(0xffffff, settings.ambientIntensity); + scene.add(ambientLight); + + pointLight = new THREE.PointLight(0xff0000, 1.5, 0, 0); + pointLight.position.z = 2500; + scene.add(pointLight); + + const pointLight2 = new THREE.PointLight(0xff6666, 3, 0, 0); + camera.add(pointLight2); + + const pointLight3 = new THREE.PointLight(0x0000ff, 1.5, 0, 0); + pointLight3.position.x = -1000; + pointLight3.position.z = 1000; + scene.add(pointLight3); + + // env map + + const path = 'textures/cube/SwedishRoyalCastle/'; + const format = '.jpg'; + const urls = [ + path + 'px' + format, + path + 'nx' + format, + path + 'py' + format, + path + 'ny' + format, + path + 'pz' + format, + path + 'nz' + format, + ]; + + const reflectionCube = new THREE.CubeTextureLoader().load(urls); + + // textures + + const textureLoader = new THREE.TextureLoader(); + const normalMap = textureLoader.load('models/obj/ninja/normal.png'); + const aoMap = textureLoader.load('models/obj/ninja/ao.jpg'); + const displacementMap = textureLoader.load('models/obj/ninja/displacement.jpg'); + + // material + + material = new THREE.MeshStandardNodeMaterial({ + color: 0xc1c1c1, + roughness: settings.roughness, + metalness: settings.metalness, + + normalMap: normalMap, + normalScale: new THREE.Vector2(1, -1), // why does the normal map require negation in this case? + + aoMap: aoMap, + aoMapIntensity: 1, + + displacementMap: displacementMap, + displacementScale: settings.displacementScale, + displacementBias: -0.428408, // from original model + + envMap: reflectionCube, + envMapIntensity: settings.envMapIntensity, + + side: THREE.DoubleSide, + }); + + // + + const loader = new OBJLoader(); + loader.load('models/obj/ninja/ninjaHead_Low.obj', function (group) { + const geometry = group.children[0].geometry; + geometry.center(); + + mesh = new THREE.Mesh(geometry, material); + mesh.scale.multiplyScalar(25); + scene.add(mesh); + }); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const aspect = window.innerWidth / window.innerHeight; + + camera.left = -height * aspect; + camera.right = height * aspect; + camera.top = height; + camera.bottom = -height; + + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + controls.update(); + + stats.begin(); + render(); + stats.end(); +} + +function render() { + pointLight.position.x = 2500 * Math.cos(r); + pointLight.position.z = 2500 * Math.sin(r); + + r += 0.01; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_materials_envmaps.ts b/examples-testing/examples/webgpu_materials_envmaps.ts new file mode 100644 index 000000000..f49b4ca1e --- /dev/null +++ b/examples-testing/examples/webgpu_materials_envmaps.ts @@ -0,0 +1,107 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let controls, camera, scene, renderer; +let textureEquirec, textureCube; +let sphereMesh, sphereMaterial, params; + +init(); + +function init() { + // CAMERAS + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0, 0, 2.5); + + // SCENE + + scene = new THREE.Scene(); + + // Textures + + const loader = new THREE.CubeTextureLoader(); + loader.setPath('textures/cube/Bridge2/'); + + textureCube = loader.load(['posx.jpg', 'negx.jpg', 'posy.jpg', 'negy.jpg', 'posz.jpg', 'negz.jpg']); + + const textureLoader = new THREE.TextureLoader(); + + textureEquirec = textureLoader.load('textures/2294472375_24a3b8ef46_o.jpg'); + textureEquirec.mapping = THREE.EquirectangularReflectionMapping; + textureEquirec.colorSpace = THREE.SRGBColorSpace; + + scene.background = textureCube; + + // + + const geometry = new THREE.IcosahedronGeometry(1, 15); + sphereMaterial = new THREE.MeshBasicMaterial({ envMap: textureCube }); + sphereMesh = new THREE.Mesh(geometry, sphereMaterial); + scene.add(sphereMesh); + + // + + renderer = new THREE.WebGPURenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 1.5; + controls.maxDistance = 6; + + // + + params = { + Cube: function () { + scene.background = textureCube; + + sphereMaterial.envMap = textureCube; + sphereMaterial.needsUpdate = true; + }, + Equirectangular: function () { + scene.background = textureEquirec; + + sphereMaterial.envMap = textureEquirec; + sphereMaterial.needsUpdate = true; + }, + Refraction: false, + }; + + const gui = new GUI({ width: 300 }); + gui.add(params, 'Cube'); + gui.add(params, 'Equirectangular'); + gui.add(params, 'Refraction').onChange(function (value) { + if (value) { + textureEquirec.mapping = THREE.EquirectangularRefractionMapping; + textureCube.mapping = THREE.CubeRefractionMapping; + } else { + textureEquirec.mapping = THREE.EquirectangularReflectionMapping; + textureCube.mapping = THREE.CubeReflectionMapping; + } + + sphereMaterial.needsUpdate = true; + }); + gui.open(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + camera.lookAt(scene.position); + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_materials_lightmap.ts b/examples-testing/examples/webgpu_materials_lightmap.ts new file mode 100644 index 000000000..616645aab --- /dev/null +++ b/examples-testing/examples/webgpu_materials_lightmap.ts @@ -0,0 +1,94 @@ +import * as THREE from 'three'; +import { vec4, color, positionLocal, mix } from 'three/tsl'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let container, stats; +let camera, scene, renderer; + +init(); + +async function init() { + const { innerWidth, innerHeight } = window; + + container = document.createElement('div'); + document.body.appendChild(container); + + // CAMERA + + camera = new THREE.PerspectiveCamera(40, innerWidth / innerHeight, 1, 10000); + camera.position.set(700, 200, -500); + + // SCENE + + scene = new THREE.Scene(); + + // LIGHTS + + const light = new THREE.DirectionalLight(0xd5deff); + light.position.x = 300; + light.position.y = 250; + light.position.z = -500; + scene.add(light); + + // SKYDOME + + const topColor = new THREE.Color().copy(light.color); + const bottomColor = new THREE.Color(0xffffff); + const offset = 400; + const exponent = 0.6; + + const h = positionLocal.add(offset).normalize().y; + + const skyMat = new THREE.MeshBasicNodeMaterial(); + skyMat.colorNode = vec4(mix(color(bottomColor), color(topColor), h.max(0.0).pow(exponent)), 1.0); + skyMat.side = THREE.BackSide; + + const sky = new THREE.Mesh(new THREE.SphereGeometry(4000, 32, 15), skyMat); + scene.add(sky); + + // MODEL + + const loader = new THREE.ObjectLoader(); + const object = await loader.loadAsync('models/json/lightmap/lightmap.json'); + scene.add(object); + + // RENDERER + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setAnimationLoop(animate); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(innerWidth, innerHeight); + container.appendChild(renderer.domElement); + + // CONTROLS + + const controls = new OrbitControls(camera, renderer.domElement); + controls.maxPolarAngle = (0.9 * Math.PI) / 2; + controls.enableZoom = false; + + // STATS + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + renderer.render(scene, camera); + stats.update(); +} diff --git a/examples-testing/examples/webgpu_materials_toon.ts b/examples-testing/examples/webgpu_materials_toon.ts new file mode 100644 index 000000000..217460596 --- /dev/null +++ b/examples-testing/examples/webgpu_materials_toon.ts @@ -0,0 +1,148 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { FontLoader } from 'three/addons/loaders/FontLoader.js'; +import { TextGeometry } from 'three/addons/geometries/TextGeometry.js'; + +let container, stats; + +let camera, scene, renderer; +let particleLight; + +const loader = new FontLoader(); +loader.load('fonts/gentilis_regular.typeface.json', function (font) { + init(font); +}); + +function init(font) { + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 2500); + camera.position.set(0.0, 400, 400 * 3.5); + + // + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x444488); + + // + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(render); + container.appendChild(renderer.domElement); + + // Materials + + const cubeWidth = 400; + const numberOfSphersPerSide = 5; + const sphereRadius = (cubeWidth / numberOfSphersPerSide) * 0.8 * 0.5; + const stepSize = 1.0 / numberOfSphersPerSide; + + const geometry = new THREE.SphereGeometry(sphereRadius, 32, 16); + + for (let alpha = 0, alphaIndex = 0; alpha <= 1.0; alpha += stepSize, alphaIndex++) { + const colors = new Uint8Array(alphaIndex + 2); + + for (let c = 0; c <= colors.length; c++) { + colors[c] = (c / colors.length) * 256; + } + + const gradientMap = new THREE.DataTexture(colors, colors.length, 1, THREE.RedFormat); + gradientMap.needsUpdate = true; + + for (let beta = 0; beta <= 1.0; beta += stepSize) { + for (let gamma = 0; gamma <= 1.0; gamma += stepSize) { + // basic monochromatic energy preservation + const diffuseColor = new THREE.Color() + .setHSL(alpha, 0.5, gamma * 0.5 + 0.1) + .multiplyScalar(1 - beta * 0.2); + + const material = new THREE.MeshToonNodeMaterial({ + color: diffuseColor, + gradientMap: gradientMap, + }); + + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.x = alpha * 400 - 200; + mesh.position.y = beta * 400 - 200; + mesh.position.z = gamma * 400 - 200; + + scene.add(mesh); + } + } + } + + function addLabel(name, location) { + const textGeo = new TextGeometry(name, { + font: font, + + size: 20, + depth: 1, + curveSegments: 1, + }); + + const textMaterial = new THREE.MeshBasicNodeMaterial(); + const textMesh = new THREE.Mesh(textGeo, textMaterial); + textMesh.position.copy(location); + scene.add(textMesh); + } + + addLabel('-gradientMap', new THREE.Vector3(-350, 0, 0)); + addLabel('+gradientMap', new THREE.Vector3(350, 0, 0)); + + addLabel('-diffuse', new THREE.Vector3(0, 0, -300)); + addLabel('+diffuse', new THREE.Vector3(0, 0, 300)); + + particleLight = new THREE.Mesh( + new THREE.SphereGeometry(4, 8, 8), + new THREE.MeshBasicNodeMaterial({ color: 0xffffff }), + ); + scene.add(particleLight); + + // Lights + + scene.add(new THREE.AmbientLight(0xc1c1c1, 3)); + + const pointLight = new THREE.PointLight(0xffffff, 2, 800, 0); + particleLight.add(pointLight); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 200; + controls.maxDistance = 2000; + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function render() { + const timer = Date.now() * 0.00025; + + particleLight.position.x = Math.sin(timer * 7) * 300; + particleLight.position.y = Math.cos(timer * 5) * 400; + particleLight.position.z = Math.cos(timer * 3) * 300; + + stats.begin(); + + renderer.render(scene, camera); + + stats.end(); +} diff --git a/examples-testing/examples/webgpu_materials_transmission.ts b/examples-testing/examples/webgpu_materials_transmission.ts new file mode 100644 index 000000000..0e04ddad9 --- /dev/null +++ b/examples-testing/examples/webgpu_materials_transmission.ts @@ -0,0 +1,168 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +const params = { + color: 0xffffff, + transmission: 1, + opacity: 1, + metalness: 0, + roughness: 0, + ior: 1.5, + thickness: 0.01, + specularIntensity: 1, + specularColor: 0xffffff, + envMapIntensity: 1, + lightIntensity: 1, + exposure: 1, +}; + +let camera, scene, renderer; + +let mesh; + +const hdrEquirect = new RGBELoader().setPath('textures/equirectangular/').load('royal_esplanade_1k.hdr', function () { + hdrEquirect.mapping = THREE.EquirectangularReflectionMapping; + + init(); + render(); +}); + +function init() { + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(render); + document.body.appendChild(renderer.domElement); + + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = params.exposure; + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 2000); + camera.position.set(0, 0, 120); + + // + + scene.background = hdrEquirect; + + // + + const geometry = new THREE.SphereGeometry(20, 64, 32); + + const texture = new THREE.CanvasTexture(generateTexture()); + texture.magFilter = THREE.NearestFilter; + texture.wrapT = THREE.RepeatWrapping; + texture.wrapS = THREE.RepeatWrapping; + texture.repeat.set(1, 3.5); + + const material = new THREE.MeshPhysicalMaterial({ + color: params.color, + metalness: params.metalness, + roughness: params.roughness, + ior: params.ior, + alphaMap: texture, + envMap: hdrEquirect, + envMapIntensity: params.envMapIntensity, + transmission: params.transmission, // use material.transmission for glass materials + specularIntensity: params.specularIntensity, + specularColor: params.specularColor, + opacity: params.opacity, + side: THREE.DoubleSide, + transparent: true, + }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 10; + controls.maxDistance = 150; + + window.addEventListener('resize', onWindowResize); + + // + + const gui = new GUI(); + + gui.addColor(params, 'color').onChange(function () { + material.color.set(params.color); + }); + + gui.add(params, 'transmission', 0, 1, 0.01).onChange(function () { + material.transmission = params.transmission; + }); + + gui.add(params, 'opacity', 0, 1, 0.01).onChange(function () { + material.opacity = params.opacity; + }); + + gui.add(params, 'metalness', 0, 1, 0.01).onChange(function () { + material.metalness = params.metalness; + }); + + gui.add(params, 'roughness', 0, 1, 0.01).onChange(function () { + material.roughness = params.roughness; + }); + + gui.add(params, 'ior', 1, 2, 0.01).onChange(function () { + material.ior = params.ior; + }); + + gui.add(params, 'thickness', 0, 5, 0.01).onChange(function () { + material.thickness = params.thickness; + }); + + gui.add(params, 'specularIntensity', 0, 1, 0.01).onChange(function () { + material.specularIntensity = params.specularIntensity; + }); + + gui.addColor(params, 'specularColor').onChange(function () { + material.specularColor.set(params.specularColor); + }); + + gui.add(params, 'envMapIntensity', 0, 1, 0.01) + .name('envMap intensity') + .onChange(function () { + material.envMapIntensity = params.envMapIntensity; + }); + + gui.add(params, 'exposure', 0, 1, 0.01).onChange(function () { + renderer.toneMappingExposure = params.exposure; + }); + + gui.open(); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); +} + +// + +function generateTexture() { + const canvas = document.createElement('canvas'); + canvas.width = 2; + canvas.height = 2; + + const context = canvas.getContext('2d'); + context.fillStyle = 'white'; + context.fillRect(0, 1, 2, 1); + + return canvas; +} + +function render() { + renderer.renderAsync(scene, camera); +} diff --git a/examples-testing/examples/webgpu_materials_video.ts b/examples-testing/examples/webgpu_materials_video.ts new file mode 100644 index 000000000..bd84aba0f --- /dev/null +++ b/examples-testing/examples/webgpu_materials_video.ts @@ -0,0 +1,184 @@ +import * as THREE from 'three'; + +let container; + +let camera, scene, renderer; + +let video, texture, material, mesh; + +let mouseX = 0; +let mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +let cube_count; + +const meshes = [], + materials = [], + xgrid = 20, + ygrid = 10; + +const startButton = document.getElementById('startButton'); +startButton.addEventListener('click', function () { + init(); +}); + +function init() { + const overlay = document.getElementById('overlay'); + overlay.remove(); + + container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 10000); + camera.position.z = 500; + + scene = new THREE.Scene(); + + const light = new THREE.DirectionalLight(0xffffff, 7); + light.position.set(0.5, 1, 1).normalize(); + scene.add(light); + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(render); + container.appendChild(renderer.domElement); + + video = document.getElementById('video'); + video.play(); + video.addEventListener('play', function () { + this.currentTime = 3; + }); + + texture = new THREE.VideoTexture(video); + + // + + let i, j, ox, oy, geometry; + + const ux = 1 / xgrid; + const uy = 1 / ygrid; + + const xsize = 480 / xgrid; + const ysize = 204 / ygrid; + + const parameters = { color: 0xffffff, map: texture }; + + cube_count = 0; + + for (i = 0; i < xgrid; i++) { + for (j = 0; j < ygrid; j++) { + ox = i; + oy = j; + + geometry = new THREE.BoxGeometry(xsize, ysize, xsize); + + change_uvs(geometry, ux, uy, ox, oy); + + materials[cube_count] = new THREE.MeshPhongMaterial(parameters); + + material = materials[cube_count]; + + material.hue = i / xgrid; + material.saturation = 1 - j / ygrid; + + material.color.setHSL(material.hue, material.saturation, 0.5); + + mesh = new THREE.Mesh(geometry, material); + + mesh.position.x = (i - xgrid / 2) * xsize; + mesh.position.y = (j - ygrid / 2) * ysize; + mesh.position.z = 0; + + mesh.scale.x = mesh.scale.y = mesh.scale.z = 1; + + scene.add(mesh); + + mesh.dx = 0.001 * (0.5 - Math.random()); + mesh.dy = 0.001 * (0.5 - Math.random()); + + meshes[cube_count] = mesh; + + cube_count += 1; + } + } + + document.addEventListener('mousemove', onDocumentMouseMove); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function change_uvs(geometry, unitx, unity, offsetx, offsety) { + const uvs = geometry.attributes.uv.array; + + for (let i = 0; i < uvs.length; i += 2) { + uvs[i] = (uvs[i] + offsetx) * unitx; + uvs[i + 1] = (uvs[i + 1] + offsety) * unity; + } +} + +function onDocumentMouseMove(event) { + mouseX = event.clientX - windowHalfX; + mouseY = (event.clientY - windowHalfY) * 0.3; +} + +// + +let h, + counter = 1; + +function render() { + const time = Date.now() * 0.00005; + + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y += (-mouseY - camera.position.y) * 0.05; + + camera.lookAt(scene.position); + + for (let i = 0; i < cube_count; i++) { + material = materials[i]; + + h = ((360 * (material.hue + time)) % 360) / 360; + material.color.setHSL(h, material.saturation, 0.5); + } + + if (counter % 1000 > 200) { + for (let i = 0; i < cube_count; i++) { + mesh = meshes[i]; + + mesh.rotation.x += 10 * mesh.dx; + mesh.rotation.y += 10 * mesh.dy; + + mesh.position.x -= 150 * mesh.dx; + mesh.position.y += 150 * mesh.dy; + mesh.position.z += 300 * mesh.dx; + } + } + + if (counter % 1000 === 0) { + for (let i = 0; i < cube_count; i++) { + mesh = meshes[i]; + + mesh.dx *= -1; + mesh.dy *= -1; + } + } + + counter++; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_mesh_batch.ts b/examples-testing/examples/webgpu_mesh_batch.ts new file mode 100644 index 000000000..a619f430b --- /dev/null +++ b/examples-testing/examples/webgpu_mesh_batch.ts @@ -0,0 +1,271 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { radixSort } from 'three/addons/utils/SortUtils.js'; + +let camera, scene, renderer; +let controls, stats; +let gui; +let geometries, mesh, material; +const ids = []; + +const matrix = new THREE.Matrix4(); + +// + +const position = new THREE.Vector3(); +const rotation = new THREE.Euler(); +const quaternion = new THREE.Quaternion(); +const scale = new THREE.Vector3(); + +// + +const MAX_GEOMETRY_COUNT = 20000; + +const api = { + webgpu: true, + count: 512, + dynamic: 16, + + sortObjects: true, + perObjectFrustumCulled: true, + opacity: 1, + useCustomSort: true, +}; + +init(); + +// + +function randomizeMatrix(matrix) { + position.x = Math.random() * 40 - 20; + position.y = Math.random() * 40 - 20; + position.z = Math.random() * 40 - 20; + + rotation.x = Math.random() * 2 * Math.PI; + rotation.y = Math.random() * 2 * Math.PI; + rotation.z = Math.random() * 2 * Math.PI; + + quaternion.setFromEuler(rotation); + + scale.x = scale.y = scale.z = 0.5 + Math.random() * 0.5; + + return matrix.compose(position, quaternion, scale); +} + +function randomizeRotationSpeed(rotation) { + rotation.x = Math.random() * 0.01; + rotation.y = Math.random() * 0.01; + rotation.z = Math.random() * 0.01; + return rotation; +} + +function initGeometries() { + geometries = [ + new THREE.ConeGeometry(1.0, 2.0), + new THREE.BoxGeometry(2.0, 2.0, 2.0), + new THREE.SphereGeometry(1.0, 16, 8), + ]; +} + +function createMaterial() { + if (!material) { + material = new THREE.MeshNormalNodeMaterial(); + } + + return material; +} + +function cleanup() { + if (mesh) { + mesh.parent.remove(mesh); + + if (mesh.dispose) { + mesh.dispose(); + } + } +} + +function initMesh() { + cleanup(); + initBatchedMesh(); +} + +function initBatchedMesh() { + const geometryCount = api.count; + const vertexCount = geometries.length * 512; + const indexCount = geometries.length * 1024; + + const euler = new THREE.Euler(); + const matrix = new THREE.Matrix4(); + mesh = new THREE.BatchedMesh(geometryCount, vertexCount, indexCount, createMaterial()); + mesh.userData.rotationSpeeds = []; + + // disable full-object frustum culling since all of the objects can be dynamic. + mesh.frustumCulled = false; + + ids.length = 0; + + const geometryIds = [ + mesh.addGeometry(geometries[0]), + mesh.addGeometry(geometries[1]), + mesh.addGeometry(geometries[2]), + ]; + for (let i = 0; i < api.count; i++) { + const id = mesh.addInstance(geometryIds[i % geometryIds.length]); + mesh.setMatrixAt(id, randomizeMatrix(matrix)); + + const rotationMatrix = new THREE.Matrix4(); + rotationMatrix.makeRotationFromEuler(randomizeRotationSpeed(euler)); + mesh.userData.rotationSpeeds.push(rotationMatrix); + + ids.push(id); + } + + scene.add(mesh); +} + +function init(forceWebGL = false) { + if (renderer) { + renderer.dispose(); + controls.dispose(); + document.body.removeChild(stats.dom); + document.body.removeChild(renderer.domElement); + } + + // camera + + const aspect = window.innerWidth / window.innerHeight; + + camera = new THREE.PerspectiveCamera(70, aspect, 1, 100); + camera.position.z = 50; + + // renderer + + renderer = new THREE.WebGPURenderer({ antialias: true, forceWebGL }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + + renderer.setAnimationLoop(animate); + + // scene + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + + if (forceWebGL) { + scene.background = new THREE.Color(0xf10000); + } else { + scene.background = new THREE.Color(0x0000f1); + } + + document.body.appendChild(renderer.domElement); + + initGeometries(); + initMesh(); + + // controls + + controls = new OrbitControls(camera, renderer.domElement); + controls.autoRotate = true; + controls.autoRotateSpeed = 1.0; + + // stats + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // gui + + gui = new GUI(); + gui.add(api, 'webgpu').onChange(() => { + init(!api.webgpu); + }); + gui.add(api, 'count', 1, MAX_GEOMETRY_COUNT).step(1).onChange(initMesh); + gui.add(api, 'dynamic', 0, MAX_GEOMETRY_COUNT).step(1); + + gui.add(api, 'opacity', 0, 1).onChange(v => { + if (v < 1) { + material.transparent = true; + material.depthWrite = false; + } else { + material.transparent = false; + material.depthWrite = true; + } + + material.opacity = v; + material.needsUpdate = true; + }); + gui.add(api, 'sortObjects'); + gui.add(api, 'perObjectFrustumCulled'); + gui.add(api, 'useCustomSort'); + + // listeners + + window.addEventListener('resize', onWindowResize); + + function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + } + + async function animate() { + animateMeshes(); + + controls.update(); + + if (mesh.isBatchedMesh) { + mesh.sortObjects = api.sortObjects; + mesh.perObjectFrustumCulled = api.perObjectFrustumCulled; + mesh.setCustomSort(api.useCustomSort ? sortFunction : null); + } + + await renderer.renderAsync(scene, camera); + + stats.update(); + } + + function animateMeshes() { + const loopNum = Math.min(api.count, api.dynamic); + + for (let i = 0; i < loopNum; i++) { + const rotationMatrix = mesh.userData.rotationSpeeds[i]; + const id = ids[i]; + + mesh.getMatrixAt(id, matrix); + matrix.multiply(rotationMatrix); + mesh.setMatrixAt(id, matrix); + } + } +} + +// + +function sortFunction(list, camera) { + // initialize options + this._options = this._options || { + get: el => el.z, + aux: new Array(this.maxInstanceCount), + }; + + const options = this._options; + options.reversed = this.material.transparent; + + // convert depth to unsigned 32 bit range + const factor = (2 ** 32 - 1) / camera.far; // UINT32_MAX / max_depth + for (let i = 0, l = list.length; i < l; i++) { + list[i].z *= factor; + } + + // perform a fast-sort using the hybrid radix sort function + radixSort(list, options); +} diff --git a/examples-testing/examples/webgpu_morphtargets.ts b/examples-testing/examples/webgpu_morphtargets.ts new file mode 100644 index 000000000..9fb7075cb --- /dev/null +++ b/examples-testing/examples/webgpu_morphtargets.ts @@ -0,0 +1,121 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let container, camera, scene, renderer, mesh; + +init(); + +function init() { + container = document.getElementById('container'); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x8fbcd4); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 20); + camera.position.z = 10; + scene.add(camera); + + scene.add(new THREE.AmbientLight(0x8fbcd4, 1.5)); + + const pointLight = new THREE.PointLight(0xffffff, 200); + camera.add(pointLight); + + const geometry = createGeometry(); + + const material = new THREE.MeshPhongMaterial({ + color: 0xff0000, + flatShading: true, + }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + initGUI(); + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(function () { + renderer.render(scene, camera); + }); + container.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableZoom = false; + + window.addEventListener('resize', onWindowResize); +} + +function createGeometry() { + const geometry = new THREE.BoxGeometry(2, 2, 2, 32, 32, 32); + + // create an empty array to hold targets for the attribute we want to morph + // morphing positions and normals is supported + geometry.morphAttributes.position = []; + + // the original positions of the cube's vertices + const positionAttribute = geometry.attributes.position; + + // for the first morph target we'll move the cube's vertices onto the surface of a sphere + const spherePositions = []; + + // for the second morph target, we'll twist the cubes vertices + const twistPositions = []; + const direction = new THREE.Vector3(1, 0, 0); + const vertex = new THREE.Vector3(); + + for (let i = 0; i < positionAttribute.count; i++) { + const x = positionAttribute.getX(i); + const y = positionAttribute.getY(i); + const z = positionAttribute.getZ(i); + + spherePositions.push( + x * Math.sqrt(1 - (y * y) / 2 - (z * z) / 2 + (y * y * z * z) / 3), + y * Math.sqrt(1 - (z * z) / 2 - (x * x) / 2 + (z * z * x * x) / 3), + z * Math.sqrt(1 - (x * x) / 2 - (y * y) / 2 + (x * x * y * y) / 3), + ); + + // stretch along the x-axis so we can see the twist better + vertex.set(x * 2, y, z); + + vertex.applyAxisAngle(direction, (Math.PI * x) / 2).toArray(twistPositions, twistPositions.length); + } + + // add the spherical positions as the first morph target + geometry.morphAttributes.position[0] = new THREE.Float32BufferAttribute(spherePositions, 3); + + // add the twisted positions as the second morph target + geometry.morphAttributes.position[1] = new THREE.Float32BufferAttribute(twistPositions, 3); + + return geometry; +} + +function initGUI() { + // Set up dat.GUI to control targets + const params = { + Spherify: 0, + Twist: 0, + }; + const gui = new GUI({ title: 'Morph Targets' }); + + gui.add(params, 'Spherify', 0, 1) + .step(0.01) + .onChange(function (value) { + mesh.morphTargetInfluences[0] = value; + }); + gui.add(params, 'Twist', 0, 1) + .step(0.01) + .onChange(function (value) { + mesh.morphTargetInfluences[1] = value; + }); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} diff --git a/examples-testing/examples/webgpu_morphtargets_face.ts b/examples-testing/examples/webgpu_morphtargets_face.ts new file mode 100644 index 000000000..ea9f86588 --- /dev/null +++ b/examples-testing/examples/webgpu_morphtargets_face.ts @@ -0,0 +1,102 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'; +import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +init(); + +async function init() { + let mixer; + + const clock = new THREE.Clock(); + + const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 20); + camera.position.set(-1.8, 0.8, 3); + + const scene = new THREE.Scene(); + + const renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + document.body.appendChild(renderer.domElement); + + await renderer.init(); + + const environment = new RoomEnvironment(); + const pmremGenerator = new THREE.PMREMGenerator(renderer); + + scene.background = new THREE.Color(0x666666); + scene.environment = pmremGenerator.fromScene(environment).texture; + + const ktx2Loader = await new KTX2Loader().setTranscoderPath('jsm/libs/basis/').detectSupportAsync(renderer); + + new GLTFLoader() + .setKTX2Loader(ktx2Loader) + .setMeshoptDecoder(MeshoptDecoder) + .load('models/gltf/facecap.glb', gltf => { + const mesh = gltf.scene.children[0]; + + scene.add(mesh); + + mixer = new THREE.AnimationMixer(mesh); + + mixer.clipAction(gltf.animations[0]).play(); + + // GUI + + const head = mesh.getObjectByName('mesh_2'); + const influences = head.morphTargetInfluences; + + const gui = new GUI(); + gui.close(); + + for (const [key, value] of Object.entries(head.morphTargetDictionary)) { + gui.add(influences, value, 0, 1, 0.01).name(key.replace('blendShape1.', '')).listen(); + } + }); + + scene.background = new THREE.Color(0x666666); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.minDistance = 2.5; + controls.maxDistance = 5; + controls.minAzimuthAngle = -Math.PI / 2; + controls.maxAzimuthAngle = Math.PI / 2; + controls.maxPolarAngle = Math.PI / 1.8; + controls.target.set(0, 0.15, -0.2); + + const stats = new Stats(); + document.body.appendChild(stats.dom); + + function animate() { + const delta = clock.getDelta(); + + if (mixer) { + mixer.update(delta); + } + + renderer.render(scene, camera); + + controls.update(); + + stats.update(); + } + + window.addEventListener('resize', () => { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + }); +} diff --git a/examples-testing/examples/webgpu_mrt.ts b/examples-testing/examples/webgpu_mrt.ts new file mode 100644 index 000000000..371c898c7 --- /dev/null +++ b/examples-testing/examples/webgpu_mrt.ts @@ -0,0 +1,119 @@ +import * as THREE from 'three'; +import { + output, + transformedNormalWorld, + pass, + step, + diffuseColor, + emissive, + viewportTopLeft, + mix, + mrt, + Fn, +} from 'three/tsl'; + +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +let camera, scene, renderer; +let postProcessing; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + // scene + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 20); + camera.position.set(-1.8, 0.6, 2.7); + + scene = new THREE.Scene(); + + new RGBELoader().setPath('textures/equirectangular/').load('royal_esplanade_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + scene.environment = texture; + + // model + + const loader = new GLTFLoader().setPath('models/gltf/DamagedHelmet/glTF/'); + loader.load('DamagedHelmet.gltf', function (gltf) { + scene.add(gltf.scene); + }); + }); + + // renderer + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(render); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + container.appendChild(renderer.domElement); + + // post processing + + const scenePass = pass(scene, camera, { minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter }); + scenePass.setMRT( + mrt({ + output: output, + normal: transformedNormalWorld.directionToColor(), + diffuse: diffuseColor, + emissive: emissive, + }), + ); + + // optimize textures + + const normalTexture = scenePass.getTexture('normal'); + const diffuseTexture = scenePass.getTexture('diffuse'); + const emissiveTexture = scenePass.getTexture('emissive'); + + normalTexture.type = diffuseTexture.type = emissiveTexture.type = THREE.UnsignedByteType; + + // post processing - mrt + + postProcessing = new THREE.PostProcessing(renderer); + postProcessing.outputColorTransform = false; + postProcessing.outputNode = Fn(() => { + const output = scenePass.getTextureNode('output'); // output name is optional here + const normal = scenePass.getTextureNode('normal'); + const diffuse = scenePass.getTextureNode('diffuse'); + const emissive = scenePass.getTextureNode('emissive'); + + const out = mix(output.renderOutput(), output, step(0.2, viewportTopLeft.x)); + const nor = mix(out, normal, step(0.4, viewportTopLeft.x)); + const emi = mix(nor, emissive, step(0.6, viewportTopLeft.x)); + const dif = mix(emi, diffuse, step(0.8, viewportTopLeft.x)); + + return dif; + })(); + + // controls + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 2; + controls.maxDistance = 10; + controls.target.set(0, 0, -0.2); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function render() { + postProcessing.render(); +} diff --git a/examples-testing/examples/webgpu_multiple_rendertargets_readback.ts b/examples-testing/examples/webgpu_multiple_rendertargets_readback.ts new file mode 100644 index 000000000..2405b8ad6 --- /dev/null +++ b/examples-testing/examples/webgpu_multiple_rendertargets_readback.ts @@ -0,0 +1,156 @@ +import * as THREE from 'three'; +import { mix, step, texture, viewportTopLeft, mrt, output, transformedNormalWorld, uv, vec2 } from 'three/tsl'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, torus; +let quadMesh, sceneMRT, renderTarget, readbackTarget, material, readbackMaterial, pixelBuffer, pixelBufferTexture; + +const gui = new GUI(); + +const options = { + selection: 'mrt', +}; + +gui.add(options, 'selection', ['mrt', 'diffuse', 'normal']); + +init(); + +function init() { + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(render); + document.body.appendChild(renderer.domElement); + + // Create a multi render target with Float buffers + + renderTarget = new THREE.RenderTarget( + window.innerWidth * window.devicePixelRatio, + window.innerHeight * window.devicePixelRatio, + { count: 2, minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter }, + ); + + // Name our G-Buffer attachments for debugging + + renderTarget.textures[0].name = 'output'; + renderTarget.textures[1].name = 'normal'; + + // Init readback render target, readback data texture, readback material + // Be careful with the size! 512 is already big. Reading data back from the GPU is computationally intensive + + const size = 512; + + readbackTarget = new THREE.RenderTarget(size, size, { count: 2 }); + + pixelBuffer = new Uint8Array(size ** 2 * 4).fill(0); + pixelBufferTexture = new THREE.DataTexture(pixelBuffer, size, size); + pixelBufferTexture.type = THREE.UnsignedByteType; + pixelBufferTexture.format = THREE.RGBAFormat; + + readbackMaterial = new THREE.MeshBasicNodeMaterial(); + readbackMaterial.colorNode = texture(pixelBufferTexture); + + // MRT + + sceneMRT = mrt({ + output: output, + normal: transformedNormalWorld, + }); + + // Scene + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x222222); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 50); + camera.position.z = 4; + + const loader = new THREE.TextureLoader(); + + const diffuse = loader.load('textures/hardwood2_diffuse.jpg'); + diffuse.colorSpace = THREE.SRGBColorSpace; + diffuse.wrapS = THREE.RepeatWrapping; + diffuse.wrapT = THREE.RepeatWrapping; + + const torusMaterial = new THREE.NodeMaterial(); + torusMaterial.colorNode = texture(diffuse, uv().mul(vec2(10, 4))); + + torus = new THREE.Mesh(new THREE.TorusKnotGeometry(1, 0.3, 128, 32), torusMaterial); + scene.add(torus); + + // Output + + material = new THREE.NodeMaterial(); + material.colorNode = mix( + texture(renderTarget.textures[0]), + texture(renderTarget.textures[1]), + step(0.5, viewportTopLeft.x), + ); + + quadMesh = new THREE.QuadMesh(material); + + // Controls + + new OrbitControls(camera, renderer.domElement); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); + + const dpr = renderer.getPixelRatio(); + renderTarget.setSize(window.innerWidth * dpr, window.innerHeight * dpr); +} + +async function render(time) { + const selection = options.selection; + + torus.rotation.y = (time / 1000) * 0.4; + + const isMRT = selection === 'mrt'; + + // render scene into target + renderer.setMRT(isMRT ? sceneMRT : null); + renderer.setRenderTarget(isMRT ? renderTarget : readbackTarget); + renderer.render(scene, camera); + + // render post FX + renderer.setMRT(null); + renderer.setRenderTarget(null); + + if (isMRT) { + quadMesh.material = material; + } else { + quadMesh.material = readbackMaterial; + + await readback(); + } + + quadMesh.render(renderer); +} + +async function readback() { + const width = readbackTarget.width; + const height = readbackTarget.height; + + const selection = options.selection; + + if (selection === 'diffuse') { + pixelBuffer = await renderer.readRenderTargetPixelsAsync(readbackTarget, 0, 0, width, height, 0); // zero is optional + + pixelBufferTexture.image.data = pixelBuffer; + pixelBufferTexture.needsUpdate = true; + } else if (selection === 'normal') { + pixelBuffer = await renderer.readRenderTargetPixelsAsync(readbackTarget, 0, 0, width, height, 1); + + pixelBufferTexture.image.data = pixelBuffer; + pixelBufferTexture.needsUpdate = true; + } +} diff --git a/examples-testing/examples/webgpu_ocean.ts b/examples-testing/examples/webgpu_ocean.ts new file mode 100644 index 000000000..9eb9922dd --- /dev/null +++ b/examples-testing/examples/webgpu_ocean.ts @@ -0,0 +1,161 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { WaterMesh } from 'three/addons/objects/WaterMesh.js'; +import { SkyMesh } from 'three/addons/objects/SkyMesh.js'; + +let container, stats; +let camera, scene, renderer; +let controls, water, sun, mesh; + +init(); + +function init() { + container = document.getElementById('container'); + + // + + renderer = new THREE.WebGPURenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 0.5; + container.appendChild(renderer.domElement); + + // + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 1, 20000); + camera.position.set(30, 30, 100); + + // + + sun = new THREE.Vector3(); + + // Water + + const waterGeometry = new THREE.PlaneGeometry(10000, 10000); + const loader = new THREE.TextureLoader(); + const waterNormals = loader.load('textures/waternormals.jpg'); + waterNormals.wrapS = waterNormals.wrapT = THREE.RepeatWrapping; + + water = new WaterMesh(waterGeometry, { + waterNormals: waterNormals, + sunDirection: new THREE.Vector3(), + sunColor: 0xffffff, + waterColor: 0x001e0f, + distortionScale: 3.7, + }); + + water.rotation.x = -Math.PI / 2; + + scene.add(water); + + // Skybox + + const sky = new SkyMesh(); + sky.scale.setScalar(10000); + scene.add(sky); + + sky.turbidity.value = 10; + sky.rayleigh.value = 2; + sky.mieCoefficient.value = 0.005; + sky.mieDirectionalG.value = 0.8; + + const parameters = { + elevation: 2, + azimuth: 180, + }; + + const pmremGenerator = new THREE.PMREMGenerator(renderer); + const sceneEnv = new THREE.Scene(); + + let renderTarget; + + function updateSun() { + const phi = THREE.MathUtils.degToRad(90 - parameters.elevation); + const theta = THREE.MathUtils.degToRad(parameters.azimuth); + + sun.setFromSphericalCoords(1, phi, theta); + + sky.sunPosition.value.copy(sun); + water.sunDirection.value.copy(sun).normalize(); + + if (renderTarget !== undefined) renderTarget.dispose(); + + sceneEnv.add(sky); + renderTarget = pmremGenerator.fromScene(sceneEnv); + scene.add(sky); + + scene.environment = renderTarget.texture; + } + + renderer.init().then(updateSun); + + // + + const geometry = new THREE.BoxGeometry(30, 30, 30); + const material = new THREE.MeshStandardMaterial({ roughness: 0 }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + controls = new OrbitControls(camera, renderer.domElement); + controls.maxPolarAngle = Math.PI * 0.495; + controls.target.set(0, 10, 0); + controls.minDistance = 40.0; + controls.maxDistance = 200.0; + controls.update(); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // GUI + + const gui = new GUI(); + + const folderSky = gui.addFolder('Sky'); + folderSky.add(parameters, 'elevation', 0, 90, 0.1).onChange(updateSun); + folderSky.add(parameters, 'azimuth', -180, 180, 0.1).onChange(updateSun); + folderSky.open(); + + const folderWater = gui.addFolder('Water'); + folderWater.add(water.distortionScale, 'value', 0, 8, 0.1).name('distortionScale'); + folderWater.add(water.size, 'value', 0.1, 10, 0.1).name('size'); + folderWater.open(); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + const time = performance.now() * 0.001; + + mesh.position.y = Math.sin(time) * 20 + 5; + mesh.rotation.x = time * 0.5; + mesh.rotation.z = time * 0.51; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_parallax_uv.ts b/examples-testing/examples/webgpu_parallax_uv.ts new file mode 100644 index 000000000..c11a18ed1 --- /dev/null +++ b/examples-testing/examples/webgpu_parallax_uv.ts @@ -0,0 +1,112 @@ +import * as THREE from 'three'; +import { texture, parallaxUV, uv } from 'three/tsl'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer; + +let controls; + +init(); + +async function init() { + // scene + + scene = new THREE.Scene(); + + // camera + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 50); + camera.position.set(10, 14, 10); + + // environment + + const environmentTexture = await new THREE.CubeTextureLoader() + .setPath('./textures/cube/Park2/') + .loadAsync(['posx.jpg', 'negx.jpg', 'posy.jpg', 'negy.jpg', 'posz.jpg', 'negz.jpg']); + + scene.environment = environmentTexture; + scene.background = environmentTexture; + + // textures + + const loader = new THREE.TextureLoader(); + + const topTexture = await loader.loadAsync('textures/ambientcg/Ice002_1K-JPG_Color.jpg'); + topTexture.colorSpace = THREE.SRGBColorSpace; + + const roughnessTexture = await loader.loadAsync('textures/ambientcg/Ice002_1K-JPG_Roughness.jpg'); + roughnessTexture.colorSpace = THREE.NoColorSpace; + + const normalTexture = await loader.loadAsync('textures/ambientcg/Ice002_1K-JPG_NormalGL.jpg'); + normalTexture.colorSpace = THREE.NoColorSpace; + + const displaceTexture = await loader.loadAsync('textures/ambientcg/Ice002_1K-JPG_Displacement.jpg'); + displaceTexture.colorSpace = THREE.NoColorSpace; + + // + + const bottomTexture = await loader.loadAsync('textures/ambientcg/Ice003_1K-JPG_Color.jpg'); + bottomTexture.colorSpace = THREE.SRGBColorSpace; + bottomTexture.wrapS = THREE.RepeatWrapping; + bottomTexture.wrapT = THREE.RepeatWrapping; + + // paralax effect + + const parallaxScale = 0.3; + const offsetUV = texture(displaceTexture).mul(parallaxScale); + + const parallaxUVOffset = parallaxUV(uv(), offsetUV); + const parallaxResult = texture(bottomTexture, parallaxUVOffset); + + const iceNode = texture(topTexture).overlay(parallaxResult); + + // material + + const material = new THREE.MeshStandardNodeMaterial(); + material.colorNode = iceNode.mul(5); // increase the color intensity to 5 ( contrast ) + material.roughnessNode = texture(roughnessTexture); + material.normalMap = normalTexture; + material.metalness = 0; + + const geometry = new THREE.BoxGeometry(10, 10, 10); + + const ground = new THREE.Mesh(geometry, material); + ground.rotateX(-Math.PI / 2); + scene.add(ground); + + // renderer + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ReinhardToneMapping; + renderer.toneMappingExposure = 6; + document.body.appendChild(renderer.domElement); + + // controls + + controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 0, 0); + controls.maxDistance = 40; + controls.minDistance = 10; + controls.autoRotate = true; + controls.autoRotateSpeed = -1; + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_performance.ts b/examples-testing/examples/webgpu_performance.ts new file mode 100644 index 000000000..315eba252 --- /dev/null +++ b/examples-testing/examples/webgpu_performance.ts @@ -0,0 +1,84 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; + +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +let camera, scene, renderer, stats; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(60, 60, 60); + + scene = new THREE.Scene(); + + renderer = new THREE.WebGPURenderer({ antialias: true, forceWebGL: false }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1; + + renderer.setAnimationLoop(render); + container.appendChild(renderer.domElement); + + // + + stats = new Stats(); + document.body.appendChild(stats.dom); + + new RGBELoader().setPath('textures/equirectangular/').load('royal_esplanade_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.environment = texture; + + // model + + const dracoLoader = new DRACOLoader(); + dracoLoader.setDecoderPath('jsm/libs/draco/gltf/'); + + const loader = new GLTFLoader().setPath('models/gltf/'); + loader.setDRACOLoader(dracoLoader); + + loader.load('dungeon_warkarma.glb', async function (gltf) { + const model = gltf.scene; + + // wait until the model can be added to the scene without blocking due to shader compilation + + await renderer.compileAsync(model, camera, scene); + + scene.add(model); + }); + }); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 2; + controls.maxDistance = 60; + controls.target.set(0, 0, -0.2); + controls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function render() { + renderer.renderAsync(scene, camera); + + stats.update(); +} diff --git a/examples-testing/examples/webgpu_postprocessing.ts b/examples-testing/examples/webgpu_postprocessing.ts new file mode 100644 index 000000000..d83642f25 --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing.ts @@ -0,0 +1,77 @@ +import * as THREE from 'three'; +import { pass } from 'three/tsl'; + +let camera, renderer, postProcessing; +let object; + +init(); + +function init() { + renderer = new THREE.WebGPURenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 400; + + const scene = new THREE.Scene(); + scene.fog = new THREE.Fog(0x000000, 1, 1000); + + object = new THREE.Object3D(); + scene.add(object); + + const geometry = new THREE.SphereGeometry(1, 4, 4); + const material = new THREE.MeshPhongMaterial({ color: 0xffffff, flatShading: true }); + + for (let i = 0; i < 100; i++) { + const mesh = new THREE.Mesh(geometry, material); + mesh.position.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).normalize(); + mesh.position.multiplyScalar(Math.random() * 400); + mesh.rotation.set(Math.random() * 2, Math.random() * 2, Math.random() * 2); + mesh.scale.x = mesh.scale.y = mesh.scale.z = Math.random() * 50; + object.add(mesh); + } + + scene.add(new THREE.AmbientLight(0xcccccc)); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(1, 1, 1); + scene.add(light); + + // postprocessing + + postProcessing = new THREE.PostProcessing(renderer); + + const scenePass = pass(scene, camera); + const scenePassColor = scenePass.getTextureNode(); + + const dotScreenPass = scenePassColor.dotScreen(); + dotScreenPass.scale.value = 0.3; + + const rgbShiftPass = dotScreenPass.rgbShift(); + rgbShiftPass.amount.value = 0.001; + + postProcessing.outputNode = rgbShiftPass; + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + object.rotation.x += 0.005; + object.rotation.y += 0.01; + + postProcessing.render(); +} diff --git a/examples-testing/examples/webgpu_postprocessing_3dlut.ts b/examples-testing/examples/webgpu_postprocessing_3dlut.ts new file mode 100644 index 000000000..9d4ffa6e9 --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing_3dlut.ts @@ -0,0 +1,139 @@ +import * as THREE from 'three'; +import { pass, texture3D, uniform, renderOutput } from 'three/tsl'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; +import { LUTCubeLoader } from 'three/addons/loaders/LUTCubeLoader.js'; +import { LUT3dlLoader } from 'three/addons/loaders/LUT3dlLoader.js'; +import { LUTImageLoader } from 'three/addons/loaders/LUTImageLoader.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +const params = { + lut: 'Bourbon 64.CUBE', + intensity: 1, +}; + +const lutMap = { + 'Bourbon 64.CUBE': null, + 'Chemical 168.CUBE': null, + 'Clayton 33.CUBE': null, + 'Cubicle 99.CUBE': null, + 'Remy 24.CUBE': null, + 'Presetpro-Cinematic.3dl': null, + NeutralLUT: null, + 'B&WLUT': null, + NightLUT: null, +}; + +let gui; +let camera, scene, renderer; +let postProcessing, lutPass; + +init(); + +async function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 20); + camera.position.set(-1.8, 0.6, 2.7); + + scene = new THREE.Scene(); + + new RGBELoader().setPath('textures/equirectangular/').load('royal_esplanade_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + scene.environment = texture; + + // model + + const loader = new GLTFLoader().setPath('models/gltf/DamagedHelmet/glTF/'); + loader.load('DamagedHelmet.gltf', function (gltf) { + scene.add(gltf.scene); + }); + }); + + const lutCubeLoader = new LUTCubeLoader(); + const lutImageLoader = new LUTImageLoader(); + const lut3dlLoader = new LUT3dlLoader(); + + for (const name in lutMap) { + if (/\.CUBE$/i.test(name)) { + lutMap[name] = lutCubeLoader.loadAsync('luts/' + name); + } else if (/\LUT$/i.test(name)) { + lutMap[name] = lutImageLoader.loadAsync(`luts/${name}.png`); + } else { + lutMap[name] = lut3dlLoader.loadAsync('luts/' + name); + } + } + + const pendings = Object.values(lutMap); + await Promise.all(pendings); + + for (const name in lutMap) { + lutMap[name] = await lutMap[name]; + } + + renderer = new THREE.WebGPURenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + container.appendChild(renderer.domElement); + + // post processing + + postProcessing = new THREE.PostProcessing(renderer); + + // ignore default output color transform ( toneMapping and outputColorSpace ) + // use renderOutput() for control the sequence + + postProcessing.outputColorTransform = false; + + // scene pass + + const scenePass = pass(scene, camera); + const outputPass = renderOutput(scenePass); + + const lut = lutMap[params.lut]; + lutPass = outputPass.lut3D(texture3D(lut.texture3D), lut.texture3D.image.width, uniform(1)); + + postProcessing.outputNode = lutPass; + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 2; + controls.maxDistance = 10; + controls.target.set(0, 0, -0.2); + controls.update(); + + gui = new GUI(); + gui.add(params, 'lut', Object.keys(lutMap)); + gui.add(params, 'intensity').min(0).max(1); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + lutPass.intensityNode.value = params.intensity; + + if (lutMap[params.lut]) { + const lut = lutMap[params.lut]; + lutPass.lutNode.value = lut.texture3D; + lutPass.size.value = lut.texture3D.image.width; + } + + postProcessing.render(); +} diff --git a/examples-testing/examples/webgpu_postprocessing_afterimage.ts b/examples-testing/examples/webgpu_postprocessing_afterimage.ts new file mode 100644 index 000000000..4c3a1d66e --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing_afterimage.ts @@ -0,0 +1,70 @@ +import * as THREE from 'three'; +import { pass } from 'three/tsl'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer; +let mesh, postProcessing, combinedPass; + +const params = { + damp: 0.96, +}; + +init(); +createGUI(); + +function init() { + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 400; + + scene = new THREE.Scene(); + scene.fog = new THREE.Fog(0x000000, 1, 1000); + + const geometry = new THREE.TorusKnotGeometry(100, 30, 100, 16); + const material = new THREE.MeshNormalMaterial(); + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // postprocessing + + postProcessing = new THREE.PostProcessing(renderer); + + const scenePass = pass(scene, camera); + const scenePassColor = scenePass.getTextureNode(); + + combinedPass = scenePassColor; + combinedPass = combinedPass.afterImage(params.damp); + + postProcessing.outputNode = combinedPass; + + window.addEventListener('resize', onWindowResize); +} + +function createGUI() { + const gui = new GUI({ title: 'Damp setting' }); + gui.add(combinedPass.damp, 'value', 0, 1).step(0.001); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function render() { + mesh.rotation.x += 0.0075; + mesh.rotation.y += 0.015; + + postProcessing.render(); +} + +function animate() { + render(); +} diff --git a/examples-testing/examples/webgpu_postprocessing_ao.ts b/examples-testing/examples/webgpu_postprocessing_ao.ts new file mode 100644 index 000000000..432d641a0 --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing_ao.ts @@ -0,0 +1,204 @@ +import * as THREE from 'three'; +import { pass, mrt, output, transformedNormalView, texture, ao, denoise } from 'three/tsl'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; +import { SimplexNoise } from 'three/addons/math/SimplexNoise.js'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer, postProcessing, controls, clock, stats, mixer; + +let aoPass, denoisePass, blendPassAO, blendPassDenoise, scenePassColor; + +const params = { + distanceExponent: 1, + distanceFallOff: 1, + radius: 0.25, + scale: 1, + thickness: 1, + denoised: true, + enabled: true, + denoiseRadius: 5, + lumaPhi: 5, + depthPhi: 5, + normalPhi: 5, +}; + +init(); + +async function init() { + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 100); + camera.position.set(5, 2, 8); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xbfe3dd); + + clock = new THREE.Clock(); + + const hdrloader = new RGBELoader(); + const envMap = await hdrloader.loadAsync('textures/equirectangular/quarry_01_1k.hdr'); + envMap.mapping = THREE.EquirectangularReflectionMapping; + + scene.environment = envMap; + + renderer = new THREE.WebGPURenderer(); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + controls = new OrbitControls(camera, renderer.domElement); + controls.target.set(0, 0.5, 0); + controls.update(); + controls.enablePan = false; + controls.enableDamping = true; + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // + + postProcessing = new THREE.PostProcessing(renderer); + + const scenePass = pass(scene, camera); + scenePass.setMRT( + mrt({ + output: output, + normal: transformedNormalView, + }), + ); + + scenePassColor = scenePass.getTextureNode('output'); + const scenePassNormal = scenePass.getTextureNode('normal'); + const scenePassDepth = scenePass.getTextureNode('depth'); + + // ao + + aoPass = ao(scenePassDepth, scenePassNormal, camera); + blendPassAO = aoPass.getTextureNode().mul(scenePassColor); + + // denoise (optional) + + const noiseTexture = texture(generateNoise()); + denoisePass = denoise(aoPass.getTextureNode(), scenePassDepth, scenePassNormal, noiseTexture, camera); + blendPassDenoise = denoisePass.mul(scenePassColor); + + postProcessing.outputNode = blendPassDenoise; + + // + + const dracoLoader = new DRACOLoader(); + dracoLoader.setDecoderPath('jsm/libs/draco/'); + dracoLoader.setDecoderConfig({ type: 'js' }); + const loader = new GLTFLoader(); + loader.setDRACOLoader(dracoLoader); + loader.setPath('models/gltf/'); + + const gltf = await loader.loadAsync('LittlestTokyo.glb'); + + const model = gltf.scene; + model.position.set(1, 1, 0); + model.scale.set(0.01, 0.01, 0.01); + scene.add(model); + + mixer = new THREE.AnimationMixer(model); + mixer.clipAction(gltf.animations[0]).play(); + + window.addEventListener('resize', onWindowResize); + + // + + const gui = new GUI(); + gui.title('AO settings'); + gui.add(params, 'distanceExponent').min(1).max(4).onChange(updateParameters); + gui.add(params, 'distanceFallOff').min(0.01).max(1).onChange(updateParameters); + gui.add(params, 'radius').min(0.01).max(1).onChange(updateParameters); + gui.add(params, 'scale').min(0.01).max(2).onChange(updateParameters); + gui.add(params, 'thickness').min(0.01).max(2).onChange(updateParameters); + gui.add(params, 'denoised').onChange(updatePassChain); + gui.add(params, 'enabled').onChange(updatePassChain); + const folder = gui.addFolder('Denoise settings'); + folder.add(params, 'denoiseRadius').min(0.01).max(10).name('radius').onChange(updateParameters); + folder.add(params, 'lumaPhi').min(0.01).max(10).onChange(updateParameters); + folder.add(params, 'depthPhi').min(0.01).max(10).onChange(updateParameters); + folder.add(params, 'normalPhi').min(0.01).max(10).onChange(updateParameters); +} + +function updatePassChain() { + if (params.enabled === true) { + if (params.denoised === true) { + postProcessing.outputNode = blendPassDenoise; + } else { + postProcessing.outputNode = blendPassAO; + } + } else { + postProcessing.outputNode = scenePassColor; + } + + postProcessing.needsUpdate = true; +} + +function updateParameters() { + aoPass.distanceExponent.value = params.distanceExponent; + aoPass.distanceFallOff.value = params.distanceFallOff; + aoPass.radius.value = params.radius; + aoPass.scale.value = params.scale; + aoPass.thickness.value = params.thickness; + + denoisePass.radius.value = params.denoiseRadius; + denoisePass.lumaPhi.value = params.lumaPhi; + denoisePass.depthPhi.value = params.depthPhi; + denoisePass.normalPhi.value = params.normalPhi; +} + +function generateNoise(size = 64) { + const simplex = new SimplexNoise(); + + const arraySize = size * size * 4; + const data = new Uint8Array(arraySize); + + for (let i = 0; i < size; i++) { + for (let j = 0; j < size; j++) { + const x = i; + const y = j; + + data[(i * size + j) * 4] = (simplex.noise(x, y) * 0.5 + 0.5) * 255; + data[(i * size + j) * 4 + 1] = (simplex.noise(x + size, y) * 0.5 + 0.5) * 255; + data[(i * size + j) * 4 + 2] = (simplex.noise(x, y + size) * 0.5 + 0.5) * 255; + data[(i * size + j) * 4 + 3] = (simplex.noise(x + size, y + size) * 0.5 + 0.5) * 255; + } + } + + const noiseTexture = new THREE.DataTexture(data, size, size); + noiseTexture.wrapS = THREE.RepeatWrapping; + noiseTexture.wrapT = THREE.RepeatWrapping; + noiseTexture.needsUpdate = true; + + return noiseTexture; +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); +} + +function animate() { + const delta = clock.getDelta(); + + if (mixer) { + mixer.update(delta); + } + + controls.update(); + + postProcessing.render(); + stats.update(); +} diff --git a/examples-testing/examples/webgpu_postprocessing_bloom.ts b/examples-testing/examples/webgpu_postprocessing_bloom.ts new file mode 100644 index 000000000..f1819db49 --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing_bloom.ts @@ -0,0 +1,129 @@ +import * as THREE from 'three'; +import { pass, bloom } from 'three/tsl'; + +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +let camera, stats; +let postProcessing, renderer, mixer, clock; + +const params = { + threshold: 0, + strength: 1, + radius: 0, + exposure: 1, +}; + +init(); + +async function init() { + const container = document.getElementById('container'); + + clock = new THREE.Clock(); + + const scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 100); + camera.position.set(-5, 2.5, -3.5); + scene.add(camera); + + scene.add(new THREE.AmbientLight(0xcccccc)); + + const pointLight = new THREE.PointLight(0xffffff, 100); + camera.add(pointLight); + + const loader = new GLTFLoader(); + const gltf = await loader.loadAsync('models/gltf/PrimaryIonDrive.glb'); + + const model = gltf.scene; + scene.add(model); + + mixer = new THREE.AnimationMixer(model); + const clip = gltf.animations[0]; + mixer.clipAction(clip.optimize()).play(); + + // + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ReinhardToneMapping; + container.appendChild(renderer.domElement); + + // + + postProcessing = new THREE.PostProcessing(renderer); + postProcessing.outputColorTransform = false; + + const scenePass = pass(scene, camera); + + const outputPass = scenePass.getTextureNode(); + + const bloomPass = outputPass.bloom(); + + postProcessing.outputNode = outputPass.add(bloomPass).renderOutput(); + + // + + stats = new Stats(); + container.appendChild(stats.dom); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.maxPolarAngle = Math.PI * 0.5; + controls.minDistance = 3; + controls.maxDistance = 8; + + // + + const gui = new GUI(); + + const bloomFolder = gui.addFolder('bloom'); + + bloomFolder.add(params, 'threshold', 0.0, 1.0).onChange(function (value) { + bloomPass.threshold.value = value; + }); + + bloomFolder.add(params, 'strength', 0.0, 3.0).onChange(function (value) { + bloomPass.strength.value = value; + }); + + gui.add(params, 'radius', 0.0, 1.0) + .step(0.01) + .onChange(function (value) { + bloomPass.radius.value = value; + }); + + const toneMappingFolder = gui.addFolder('tone mapping'); + + toneMappingFolder.add(params, 'exposure', 0.1, 2).onChange(function (value) { + renderer.toneMappingExposure = Math.pow(value, 4.0); + }); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); +} + +function animate() { + const delta = clock.getDelta(); + + mixer.update(delta); + + postProcessing.render(); + + stats.update(); +} diff --git a/examples-testing/examples/webgpu_postprocessing_bloom_emissive.ts b/examples-testing/examples/webgpu_postprocessing_bloom_emissive.ts new file mode 100644 index 000000000..971868360 --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing_bloom_emissive.ts @@ -0,0 +1,101 @@ +import * as THREE from 'three'; +import { pass, mrt, output, emissive } from 'three/tsl'; + +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer; +let postProcessing; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + // + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.25, 20); + camera.position.set(-1.8, 0.6, 2.7); + + scene = new THREE.Scene(); + + new RGBELoader().setPath('textures/equirectangular/').load('moonless_golf_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + scene.environment = texture; + + // model + + const loader = new GLTFLoader().setPath('models/gltf/DamagedHelmet/glTF/'); + loader.load('DamagedHelmet.gltf', function (gltf) { + scene.add(gltf.scene); + }); + }); + + // + + renderer = new THREE.WebGPURenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(render); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + container.appendChild(renderer.domElement); + + // + + const scenePass = pass(scene, camera); + scenePass.setMRT( + mrt({ + output, + emissive, + }), + ); + + const outputPass = scenePass.getTextureNode(); + const emissivePass = scenePass.getTextureNode('emissive'); + + const bloomPass = emissivePass.bloom(2.5, 0.5); + + postProcessing = new THREE.PostProcessing(renderer); + postProcessing.outputColorTransform = false; + postProcessing.outputNode = outputPass.add(bloomPass).renderOutput(); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 2; + controls.maxDistance = 10; + controls.target.set(0, 0, -0.2); + + window.addEventListener('resize', onWindowResize); + + // + + const gui = new GUI(); + + const bloomFolder = gui.addFolder('bloom'); + bloomFolder.add(bloomPass.strength, 'value', 0.0, 5.0).name('strength'); + bloomFolder.add(bloomPass.radius, 'value', 0.0, 1.0).name('radius'); + + const toneMappingFolder = gui.addFolder('tone mapping'); + toneMappingFolder.add(renderer, 'toneMappingExposure', 0.1, 2).name('exposure'); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function render() { + postProcessing.render(); +} diff --git a/examples-testing/examples/webgpu_postprocessing_bloom_selective.ts b/examples-testing/examples/webgpu_postprocessing_bloom_selective.ts new file mode 100644 index 000000000..c180acb13 --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing_bloom_selective.ts @@ -0,0 +1,125 @@ +import * as THREE from 'three'; +import { pass, mrt, output, float, uniform } from 'three/tsl'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +// scene + +const scene = new THREE.Scene(); + +const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 200); +camera.position.set(0, 0, 20); +camera.lookAt(0, 0, 0); + +const geometry = new THREE.IcosahedronGeometry(1, 15); + +for (let i = 0; i < 50; i++) { + const color = new THREE.Color(); + color.setHSL(Math.random(), 0.7, Math.random() * 0.2 + 0.05); + + const bloomIntensity = Math.random() > 0.5 ? 1 : 0; + + const material = new THREE.MeshBasicNodeMaterial({ color: color }); + material.mrtNode = mrt({ + bloomIntensity: uniform(bloomIntensity), + }); + + const sphere = new THREE.Mesh(geometry, material); + sphere.position.x = Math.random() * 10 - 5; + sphere.position.y = Math.random() * 10 - 5; + sphere.position.z = Math.random() * 10 - 5; + sphere.position.normalize().multiplyScalar(Math.random() * 4.0 + 2.0); + sphere.scale.setScalar(Math.random() * Math.random() + 0.5); + scene.add(sphere); +} + +// renderer + +const renderer = new THREE.WebGPURenderer(); +renderer.setPixelRatio(window.devicePixelRatio); +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.setAnimationLoop(animate); +renderer.toneMapping = THREE.NeutralToneMapping; +document.body.appendChild(renderer.domElement); + +// post processing + +const postProcessing = new THREE.PostProcessing(renderer); +postProcessing.outputColorTransform = false; + +const scenePass = pass(scene, camera); + +scenePass.setMRT( + mrt({ + output, + bloomIntensity: float(0), // default bloom intensity + }), +); + +const outputPass = scenePass.getTextureNode(); + +const bloomIntensityPass = scenePass.getTextureNode('bloomIntensity'); + +const bloomPass = outputPass.mul(bloomIntensityPass).bloom(); + +postProcessing.outputNode = outputPass.add(bloomPass).renderOutput(); + +// controls + +const controls = new OrbitControls(camera, renderer.domElement); +controls.maxPolarAngle = Math.PI * 0.5; +controls.minDistance = 1; +controls.maxDistance = 100; + +// raycaster + +const raycaster = new THREE.Raycaster(); +const mouse = new THREE.Vector2(); + +window.addEventListener('pointerdown', event => { + mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; + + raycaster.setFromCamera(mouse, camera); + + const intersects = raycaster.intersectObjects(scene.children, false); + + if (intersects.length > 0) { + const material = intersects[0].object.material; + + const bloomIntensity = material.mrtNode.get('bloomIntensity'); + bloomIntensity.value = bloomIntensity.value === 0 ? 1 : 0; + } +}); + +// gui + +const gui = new GUI(); + +const bloomFolder = gui.addFolder('bloom'); +bloomFolder.add(bloomPass.threshold, 'value', 0.0, 1.0).name('threshold'); +bloomFolder.add(bloomPass.strength, 'value', 0.0, 3).name('strength'); +bloomFolder.add(bloomPass.radius, 'value', 0.0, 1.0).name('radius'); + +const toneMappingFolder = gui.addFolder('tone mapping'); +toneMappingFolder.add(renderer, 'toneMappingExposure', 0.1, 3).name('exposure'); + +// events + +window.onresize = function () { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); +}; + +// animate + +function animate() { + postProcessing.render(); +} diff --git a/examples-testing/examples/webgpu_postprocessing_difference.ts b/examples-testing/examples/webgpu_postprocessing_difference.ts new file mode 100644 index 000000000..49f9084fc --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing_difference.ts @@ -0,0 +1,92 @@ +import * as THREE from 'three'; +import { pass, luminance } from 'three/tsl'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { Timer } from 'three/addons/misc/Timer.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +const params = { + speed: 0, +}; + +let camera, renderer, postProcessing; +let timer, mesh, controls; + +init(); + +function init() { + renderer = new THREE.WebGPURenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.NeutralToneMapping; + document.body.appendChild(renderer.domElement); + + // + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 100); + camera.position.set(1, 2, 3); + + const scene = new THREE.Scene(); + scene.fog = new THREE.Fog(0x0487e2, 7, 25); + scene.background = new THREE.Color(0x0487e2); + + timer = new Timer(); + + const texture = new THREE.TextureLoader().load('textures/crate.gif'); + texture.colorSpace = THREE.SRGBColorSpace; + + const geometry = new THREE.BoxGeometry(); + const material = new THREE.MeshBasicMaterial({ map: texture }); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // post processing + + postProcessing = new THREE.PostProcessing(renderer); + + const scenePass = pass(scene, camera); + + const currentTexture = scenePass.getTextureNode(); + const previousTexture = scenePass.getPreviousTextureNode(); + + const frameDiff = previousTexture.sub(currentTexture).abs(); + + const saturationAmount = luminance(frameDiff).mul(1000).clamp(0, 3); + + postProcessing.outputNode = currentTexture.saturation(saturationAmount); + + // + + controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 2; + controls.maxDistance = 10; + controls.enableDamping = true; + controls.dampingFactor = 0.01; + + window.addEventListener('resize', onWindowResize); + + // + + const gui = new GUI(); + gui.add(params, 'speed', 0, 2); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + timer.update(); + + controls.update(); + + mesh.rotation.y += timer.getDelta() * 5 * params.speed; + + postProcessing.render(); +} diff --git a/examples-testing/examples/webgpu_postprocessing_dof.ts b/examples-testing/examples/webgpu_postprocessing_dof.ts new file mode 100644 index 000000000..3fb4046be --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing_dof.ts @@ -0,0 +1,159 @@ +import * as THREE from 'three'; +import { cubeTexture, positionWorld, oscSine, timerGlobal, pass, uniform } from 'three/tsl'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import Stats from 'three/addons/libs/stats.module.js'; + +// + +let camera, scene, renderer, mesh, stats; + +let mouseX = 0, + mouseY = 0; + +let windowHalfX = window.innerWidth / 2; +let windowHalfY = window.innerHeight / 2; + +let width = window.innerWidth; +let height = window.innerHeight; + +let postProcessing; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(70, width / height, 1, 3000); + camera.position.z = 200; + + scene = new THREE.Scene(); + + const path = 'textures/cube/SwedishRoyalCastle/'; + const format = '.jpg'; + const urls = [ + path + 'px' + format, + path + 'nx' + format, + path + 'py' + format, + path + 'ny' + format, + path + 'pz' + format, + path + 'nz' + format, + ]; + + const xgrid = 14, + ygrid = 9, + zgrid = 14; + const count = xgrid * ygrid * zgrid; + + const textureCube = new THREE.CubeTextureLoader().load(urls); + const cubeTextureNode = cubeTexture(textureCube); + const oscPos = oscSine(positionWorld.div(1000 /* scene distance */).add(timerGlobal(0.2 /* speed */))); + + const geometry = new THREE.SphereGeometry(60, 20, 10); + const material = new THREE.MeshBasicNodeMaterial(); + material.colorNode = cubeTextureNode.mul(oscPos); + + mesh = new THREE.InstancedMesh(geometry, material, count); + scene.add(mesh); + + const matrix = new THREE.Matrix4(); + + let index = 0; + + for (let i = 0; i < xgrid; i++) { + for (let j = 0; j < ygrid; j++) { + for (let k = 0; k < zgrid; k++) { + const x = 200 * (i - xgrid / 2); + const y = 200 * (j - ygrid / 2); + const z = 200 * (k - zgrid / 2); + + mesh.setMatrixAt(index, matrix.identity().setPosition(x, y, z)); + index++; + } + } + } + + // renderer + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + const effectController = { + focus: uniform(500.0), + aperture: uniform(5), + maxblur: uniform(0.01), + }; + + // post processing + + postProcessing = new THREE.PostProcessing(renderer); + + const scenePass = pass(scene, camera); + + const scenePassColor = scenePass.getTextureNode(); + const scenePassViewZ = scenePass.getViewZNode(); + + const dofPass = scenePassColor.dof( + scenePassViewZ, + effectController.focus, + effectController.aperture.mul(0.00001), + effectController.maxblur, + ); + + postProcessing.outputNode = dofPass; + + // controls + + renderer.domElement.style.touchAction = 'none'; + renderer.domElement.addEventListener('pointermove', onPointerMove); + + window.addEventListener('resize', onWindowResize); + + // stats + + stats = new Stats(); + document.body.appendChild(stats.dom); + + // gui + + const gui = new GUI(); + gui.add(effectController.focus, 'value', 10.0, 3000.0, 10).name('focus'); + gui.add(effectController.aperture, 'value', 0, 10, 0.1).name('aperture'); + gui.add(effectController.maxblur, 'value', 0.0, 0.01, 0.001).name('maxblur'); +} + +function onPointerMove(event) { + if (event.isPrimary === false) return; + + mouseX = event.clientX - windowHalfX; + mouseY = event.clientY - windowHalfY; +} + +function onWindowResize() { + windowHalfX = window.innerWidth / 2; + windowHalfY = window.innerHeight / 2; + + width = window.innerWidth; + height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); +} + +function animate() { + render(); + stats.update(); +} + +function render() { + camera.position.x += (mouseX - camera.position.x) * 0.036; + camera.position.y += (-mouseY - camera.position.y) * 0.036; + + camera.lookAt(scene.position); + + postProcessing.render(); +} diff --git a/examples-testing/examples/webgpu_postprocessing_fxaa.ts b/examples-testing/examples/webgpu_postprocessing_fxaa.ts new file mode 100644 index 000000000..5e75fa7a9 --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing_fxaa.ts @@ -0,0 +1,122 @@ +import * as THREE from 'three'; +import { pass, renderOutput } from 'three/tsl'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +const params = { + enabled: true, + animated: false, +}; + +let camera, scene, renderer, clock, group; +let postProcessing; + +init(); + +async function init() { + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200); + camera.position.z = 50; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0xffffff); + + clock = new THREE.Clock(); + + // + + const hemiLight = new THREE.HemisphereLight(0xffffff, 0x8d8d8d); + hemiLight.position.set(0, 1000, 0); + scene.add(hemiLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 3); + dirLight.position.set(-3000, 1000, -1000); + scene.add(dirLight); + + // + + group = new THREE.Group(); + + const geometry = new THREE.TetrahedronGeometry(); + const material = new THREE.MeshStandardMaterial({ color: 0xf73232, flatShading: true }); + + for (let i = 0; i < 100; i++) { + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.x = Math.random() * 50 - 25; + mesh.position.y = Math.random() * 50 - 25; + mesh.position.z = Math.random() * 50 - 25; + + mesh.scale.setScalar(Math.random() * 2 + 1); + + mesh.rotation.x = Math.random() * Math.PI; + mesh.rotation.y = Math.random() * Math.PI; + mesh.rotation.z = Math.random() * Math.PI; + + group.add(mesh); + } + + scene.add(group); + + renderer = new THREE.WebGPURenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // post processing + + postProcessing = new THREE.PostProcessing(renderer); + + // ignore default output color transform ( toneMapping and outputColorSpace ) + // use renderOutput() for control the sequence + + postProcessing.outputColorTransform = false; + + // scene pass + + const scenePass = pass(scene, camera); + const outputPass = renderOutput(scenePass); + + // FXAA must be computed in sRGB color space (so after tone mapping and color space conversion) + + const fxaaPass = outputPass.fxaa(); + postProcessing.outputNode = fxaaPass; + + // + + window.addEventListener('resize', onWindowResize); + + // + + const gui = new GUI(); + gui.title('FXAA settings'); + gui.add(params, 'enabled').onChange(value => { + if (value === true) { + postProcessing.outputNode = fxaaPass; + } else { + postProcessing.outputNode = outputPass; + } + + postProcessing.needsUpdate = true; + }); + gui.add(params, 'animated'); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + const delta = clock.getDelta(); + + if (params.animated === true) { + group.rotation.y += delta * 0.1; + } + + postProcessing.render(); +} diff --git a/examples-testing/examples/webgpu_postprocessing_masking.ts b/examples-testing/examples/webgpu_postprocessing_masking.ts new file mode 100644 index 000000000..a4f56b170 --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing_masking.ts @@ -0,0 +1,85 @@ +import * as THREE from 'three'; +import { pass, texture } from 'three/tsl'; + +let camera, postProcessing, renderer; +let box, torus; + +init(); + +function init() { + // scene + + const baseScene = new THREE.Scene(); + baseScene.background = new THREE.Color(0xe0e0e0); + + const maskScene1 = new THREE.Scene(); + box = new THREE.Mesh(new THREE.BoxGeometry(4, 4, 4)); + maskScene1.add(box); + + const maskScene2 = new THREE.Scene(); + torus = new THREE.Mesh(new THREE.TorusGeometry(3, 1, 16, 32)); + maskScene2.add(torus); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000); + camera.position.z = 10; + + // textures + + const texture1 = new THREE.TextureLoader().load('textures/758px-Canestra_di_frutta_(Caravaggio).jpg'); + texture1.colorSpace = THREE.SRGBColorSpace; + texture1.minFilter = THREE.LinearFilter; + texture1.flipY = false; + + const texture2 = new THREE.TextureLoader().load('textures/2294472375_24a3b8ef46_o.jpg'); + texture2.colorSpace = THREE.SRGBColorSpace; + texture2.flipY = false; + + // renderer + + renderer = new THREE.WebGPURenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); + + // post processing + + const base = pass(baseScene, camera); + const sceneMask1 = pass(maskScene1, camera).a; + const sceneMask2 = pass(maskScene2, camera).a; + + let compose = base; + compose = sceneMask1.mix(compose, texture(texture1)); + compose = sceneMask2.mix(compose, texture(texture2)); + + postProcessing = new THREE.PostProcessing(renderer); + postProcessing.outputNode = compose; +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); +} + +function animate() { + const time = performance.now() * 0.001 + 6000; + + box.position.x = Math.cos(time / 1.5) * 2; + box.position.y = Math.sin(time) * 2; + box.rotation.x = time; + box.rotation.y = time / 2; + + torus.position.x = Math.cos(time) * 2; + torus.position.y = Math.sin(time / 1.5) * 2; + torus.rotation.x = time; + torus.rotation.y = time / 2; + + postProcessing.render(); +} diff --git a/examples-testing/examples/webgpu_postprocessing_motion_blur.ts b/examples-testing/examples/webgpu_postprocessing_motion_blur.ts new file mode 100644 index 000000000..aa76acb9b --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing_motion_blur.ts @@ -0,0 +1,205 @@ +import * as THREE from 'three'; +import { pass, texture, motionBlur, uniform, output, mrt, mix, velocity, uv, viewportTopLeft } from 'three/tsl'; + +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let camera, scene, renderer; +let boxLeft, boxRight, model, mixer, clock; +let postProcessing; +let controls; +let stats; + +const params = { + speed: 1.0, +}; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.25, 30); + camera.position.set(0, 1.5, 4.5); + + scene = new THREE.Scene(); + scene.fog = new THREE.Fog(0x0487e2, 7, 25); + + const sunLight = new THREE.DirectionalLight(0xffe499, 5); + sunLight.castShadow = true; + sunLight.shadow.camera.near = 0.1; + sunLight.shadow.camera.far = 10; + sunLight.shadow.camera.right = 2; + sunLight.shadow.camera.left = -2; + sunLight.shadow.camera.top = 2; + sunLight.shadow.camera.bottom = -2; + sunLight.shadow.mapSize.width = 2048; + sunLight.shadow.mapSize.height = 2048; + sunLight.shadow.bias = -0.001; + sunLight.position.set(4, 4, 2); + + const waterAmbientLight = new THREE.HemisphereLight(0x333366, 0x74ccf4, 5); + const skyAmbientLight = new THREE.HemisphereLight(0x74ccf4, 0, 1); + + scene.add(sunLight); + scene.add(skyAmbientLight); + scene.add(waterAmbientLight); + + clock = new THREE.Clock(); + + // animated model + + const loader = new GLTFLoader(); + loader.load('models/gltf/Xbot.glb', function (gltf) { + model = gltf.scene; + + model.rotation.y = Math.PI / 2; + + model.traverse(function (child) { + if (child.isMesh) { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + mixer = new THREE.AnimationMixer(model); + + const action = mixer.clipAction(gltf.animations[3]); + action.play(); + + scene.add(model); + }); + + // textures + + const textureLoader = new THREE.TextureLoader(); + + const floorColor = textureLoader.load('textures/floors/FloorsCheckerboard_S_Diffuse.jpg'); + floorColor.wrapS = THREE.RepeatWrapping; + floorColor.wrapT = THREE.RepeatWrapping; + floorColor.colorSpace = THREE.SRGBColorSpace; + + const floorNormal = textureLoader.load('textures/floors/FloorsCheckerboard_S_Normal.jpg'); + floorNormal.wrapS = THREE.RepeatWrapping; + floorNormal.wrapT = THREE.RepeatWrapping; + + // floor + + const floorUV = uv().mul(5); + + const floorMaterial = new THREE.MeshPhongNodeMaterial(); + floorMaterial.colorNode = texture(floorColor, floorUV); + + const floor = new THREE.Mesh(new THREE.BoxGeometry(15, 0.001, 15), floorMaterial); + floor.receiveShadow = true; + + floor.position.set(0, 0, 0); + scene.add(floor); + + const walls = new THREE.Mesh( + new THREE.BoxGeometry(15, 15, 15), + new THREE.MeshPhongNodeMaterial({ colorNode: floorMaterial.colorNode, side: THREE.BackSide }), + ); + scene.add(walls); + + const map = new THREE.TextureLoader().load('textures/uv_grid_opengl.jpg'); + map.colorSpace = THREE.SRGBColorSpace; + + const geometry = new THREE.TorusGeometry(0.8); + const material = new THREE.MeshBasicMaterial({ map }); + + boxRight = new THREE.Mesh(geometry, material); + boxRight.position.set(3.5, 1.5, -4); + scene.add(boxRight); + + boxLeft = new THREE.Mesh(geometry, material); + boxLeft.position.set(-3.5, 1.5, -4); + scene.add(boxLeft); + + // renderer + + renderer = new THREE.WebGPURenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 1; + controls.maxDistance = 10; + controls.maxPolarAngle = Math.PI / 2; + controls.autoRotate = true; + controls.autoRotateSpeed = 1; + controls.target.set(0, 1, 0); + controls.enableDamping = true; + controls.dampingFactor = 0.05; + controls.update(); + + // post-processing + + const blurAmount = uniform(1); + const showVelocity = uniform(0); + + const scenePass = pass(scene, camera); + + scenePass.setMRT( + mrt({ + output, + velocity, + }), + ); + + const beauty = scenePass.getTextureNode(); + const vel = scenePass.getTextureNode('velocity').mul(blurAmount); + + const mBlur = motionBlur(beauty, vel); + + const vignet = viewportTopLeft.distance(0.5).remap(0.6, 1).mul(2).clamp().oneMinus(); + + postProcessing = new THREE.PostProcessing(renderer); + postProcessing.outputNode = mix(mBlur, vel, showVelocity).mul(vignet); + + // + + const gui = new GUI(); + gui.title('Motion Blur Settings'); + gui.add(controls, 'autoRotate'); + gui.add(blurAmount, 'value', 0, 3).name('blur amount'); + gui.add(params, 'speed', 0, 2); + gui.add(showVelocity, 'value', 0, 1).name('show velocity'); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + stats.update(); + + controls.update(); + + const delta = clock.getDelta(); + const speed = params.speed; + + boxRight.rotation.y += delta * 4 * speed; + boxLeft.scale.setScalar(1 + Math.sin(clock.elapsedTime * 10 * speed) * 0.2); + + if (model) { + mixer.update(delta * speed); + } + + postProcessing.render(); +} diff --git a/examples-testing/examples/webgpu_postprocessing_pixel.ts b/examples-testing/examples/webgpu_postprocessing_pixel.ts new file mode 100644 index 000000000..d7e51008d --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing_pixel.ts @@ -0,0 +1,234 @@ +import * as THREE from 'three'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +import { uniform, pixelationPass } from 'three/tsl'; + +let camera, scene, renderer, postProcessing, crystalMesh, clock; +let gui, effectController; + +init(); + +function init() { + const aspectRatio = window.innerWidth / window.innerHeight; + + camera = new THREE.OrthographicCamera(-aspectRatio, aspectRatio, 1, -1, 0.1, 10); + camera.position.y = 2 * Math.tan(Math.PI / 6); + camera.position.z = 2; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x151729); + + clock = new THREE.Clock(); + + // textures + + const loader = new THREE.TextureLoader(); + const texChecker = pixelTexture(loader.load('textures/checker.png')); + const texChecker2 = pixelTexture(loader.load('textures/checker.png')); + texChecker.repeat.set(3, 3); + texChecker2.repeat.set(1.5, 1.5); + + // meshes + + const boxMaterial = new THREE.MeshPhongMaterial({ map: texChecker2 }); + + function addBox(boxSideLength, x, z, rotation) { + const mesh = new THREE.Mesh(new THREE.BoxGeometry(boxSideLength, boxSideLength, boxSideLength), boxMaterial); + mesh.castShadow = true; + mesh.receiveShadow = true; + mesh.rotation.y = rotation; + mesh.position.y = boxSideLength / 2; + mesh.position.set(x, boxSideLength / 2 + 0.0001, z); + scene.add(mesh); + return mesh; + } + + addBox(0.4, 0, 0, Math.PI / 4); + addBox(0.5, -0.5, -0.5, Math.PI / 4); + + const planeSideLength = 2; + const planeMesh = new THREE.Mesh( + new THREE.PlaneGeometry(planeSideLength, planeSideLength), + new THREE.MeshPhongMaterial({ map: texChecker }), + ); + planeMesh.receiveShadow = true; + planeMesh.rotation.x = -Math.PI / 2; + scene.add(planeMesh); + + const radius = 0.2; + const geometry = new THREE.IcosahedronGeometry(radius); + crystalMesh = new THREE.Mesh( + geometry, + new THREE.MeshPhongMaterial({ + color: 0x68b7e9, + emissive: 0x4f7e8b, + shininess: 10, + specular: 0xffffff, + }), + ); + crystalMesh.receiveShadow = true; + crystalMesh.castShadow = true; + scene.add(crystalMesh); + + // lights + + scene.add(new THREE.AmbientLight(0x757f8e, 3)); + + const directionalLight = new THREE.DirectionalLight(0xfffecd, 1.5); + directionalLight.position.set(100, 100, 100); + directionalLight.castShadow = true; + directionalLight.shadow.mapSize.set(2048, 2048); + directionalLight.shadow.bias = -0.0001; + scene.add(directionalLight); + + const spotLight = new THREE.SpotLight(0xffc100, 10, 10, Math.PI / 16, 0.02, 2); + spotLight.position.set(2, 2, 0); + const target = spotLight.target; + scene.add(target); + target.position.set(0, 0, 0); + spotLight.castShadow = true; + spotLight.shadow.bias = -0.001; + scene.add(spotLight); + + renderer = new THREE.WebGPURenderer(); + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.BasicShadowMap; + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + effectController = { + pixelSize: uniform(6), + normalEdgeStrength: uniform(0.3), + depthEdgeStrength: uniform(0.4), + pixelAlignedPanning: true, + }; + + postProcessing = new THREE.PostProcessing(renderer); + const scenePass = pixelationPass( + scene, + camera, + effectController.pixelSize, + effectController.normalEdgeStrength, + effectController.depthEdgeStrength, + ); + postProcessing.outputNode = scenePass; + + window.addEventListener('resize', onWindowResize); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.maxZoom = 2; + + // gui + + gui = new GUI(); + gui.add(effectController.pixelSize, 'value', 1, 16, 1).name('Pixel Size'); + gui.add(effectController.normalEdgeStrength, 'value', 0, 2, 0.05).name('Normal Edge Strength'); + gui.add(effectController.depthEdgeStrength, 'value', 0, 1, 0.05).name('Depth Edge Strength'); + gui.add(effectController, 'pixelAlignedPanning'); +} + +function onWindowResize() { + const aspectRatio = window.innerWidth / window.innerHeight; + camera.left = -aspectRatio; + camera.right = aspectRatio; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const t = clock.getElapsedTime(); + + crystalMesh.material.emissiveIntensity = Math.sin(t * 3) * 0.5 + 0.5; + crystalMesh.position.y = 0.7 + Math.sin(t * 2) * 0.05; + crystalMesh.rotation.y = stopGoEased(t, 2, 4) * 2 * Math.PI; + + const rendererSize = renderer.getSize(new THREE.Vector2()); + const aspectRatio = rendererSize.x / rendererSize.y; + + if (effectController.pixelAlignedPanning) { + const pixelSize = effectController.pixelSize.value; + + pixelAlignFrustum( + camera, + aspectRatio, + Math.floor(rendererSize.x / pixelSize), + Math.floor(rendererSize.y / pixelSize), + ); + } else if (camera.left != -aspectRatio || camera.top != 1.0) { + // Reset the Camera Frustum if it has been modified + camera.left = -aspectRatio; + camera.right = aspectRatio; + camera.top = 1.0; + camera.bottom = -1.0; + camera.updateProjectionMatrix(); + } + + postProcessing.render(); +} + +// Helper functions + +function pixelTexture(texture) { + texture.minFilter = THREE.NearestFilter; + texture.magFilter = THREE.NearestFilter; + texture.generateMipmaps = false; + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + texture.colorSpace = THREE.SRGBColorSpace; + return texture; +} + +function easeInOutCubic(x) { + return x ** 2 * 3 - x ** 3 * 2; +} + +function linearStep(x, edge0, edge1) { + const w = edge1 - edge0; + const m = 1 / w; + const y0 = -m * edge0; + return THREE.MathUtils.clamp(y0 + m * x, 0, 1); +} + +function stopGoEased(x, downtime, period) { + const cycle = (x / period) | 0; + const tween = x - cycle * period; + const linStep = easeInOutCubic(linearStep(tween, downtime, period)); + return cycle + linStep; +} + +function pixelAlignFrustum(camera, aspectRatio, pixelsPerScreenWidth, pixelsPerScreenHeight) { + // 0. Get Pixel Grid Units + const worldScreenWidth = (camera.right - camera.left) / camera.zoom; + const worldScreenHeight = (camera.top - camera.bottom) / camera.zoom; + const pixelWidth = worldScreenWidth / pixelsPerScreenWidth; + const pixelHeight = worldScreenHeight / pixelsPerScreenHeight; + + // 1. Project the current camera position along its local rotation bases + const camPos = new THREE.Vector3(); + camera.getWorldPosition(camPos); + const camRot = new THREE.Quaternion(); + camera.getWorldQuaternion(camRot); + const camRight = new THREE.Vector3(1.0, 0.0, 0.0).applyQuaternion(camRot); + const camUp = new THREE.Vector3(0.0, 1.0, 0.0).applyQuaternion(camRot); + const camPosRight = camPos.dot(camRight); + const camPosUp = camPos.dot(camUp); + + // 2. Find how far along its position is along these bases in pixel units + const camPosRightPx = camPosRight / pixelWidth; + const camPosUpPx = camPosUp / pixelHeight; + + // 3. Find the fractional pixel units and convert to world units + const fractX = camPosRightPx - Math.round(camPosRightPx); + const fractY = camPosUpPx - Math.round(camPosUpPx); + + // 4. Add fractional world units to the left/right top/bottom to align with the pixel grid + camera.left = -aspectRatio - fractX * pixelWidth; + camera.right = aspectRatio - fractX * pixelWidth; + camera.top = 1.0 - fractY * pixelHeight; + camera.bottom = -1.0 - fractY * pixelHeight; + camera.updateProjectionMatrix(); +} diff --git a/examples-testing/examples/webgpu_postprocessing_sobel.ts b/examples-testing/examples/webgpu_postprocessing_sobel.ts new file mode 100644 index 000000000..cbc5a96ba --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing_sobel.ts @@ -0,0 +1,89 @@ +import * as THREE from 'three'; +import { pass } from 'three/tsl'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer; +let postProcessing; + +const params = { + enable: true, +}; + +init(); + +function init() { + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0, 1, 3); + camera.lookAt(scene.position); + + // + + const ambientLight = new THREE.AmbientLight(0xe7e7e7); + scene.add(ambientLight); + + const pointLight = new THREE.PointLight(0xffffff, 20); + camera.add(pointLight); + scene.add(camera); + + // + + const geometry = new THREE.TorusKnotGeometry(1, 0.3, 256, 32); + const material = new THREE.MeshPhongMaterial({ color: 0xffff00 }); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.outputColorSpace = THREE.LinearSRGBColorSpace; + document.body.appendChild(renderer.domElement); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableZoom = false; + + // postprocessing + + postProcessing = new THREE.PostProcessing(renderer); + + const scenePass = pass(scene, camera); + const scenePassColor = scenePass.getTextureNode(); + + postProcessing.outputNode = scenePassColor.sobel(); + + // + + const gui = new GUI(); + + gui.add(params, 'enable'); + gui.open(); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + if (params.enable === true) { + postProcessing.render(); + } else { + renderer.render(scene, camera); + } +} diff --git a/examples-testing/examples/webgpu_postprocessing_ssaa.ts b/examples-testing/examples/webgpu_postprocessing_ssaa.ts new file mode 100644 index 000000000..76e3a95cd --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing_ssaa.ts @@ -0,0 +1,181 @@ +import * as THREE from 'three'; +import { ssaaPass } from 'three/tsl'; + +import { Timer } from 'three/addons/misc/Timer.js'; +import Stats from 'three/addons/libs/stats.module.js'; +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let scene, mesh, renderer, postProcessing; +let camera, ssaaRenderPass; +let gui, stats, timer; + +const params = { + sampleLevel: 3, + camera: 'perspective', + clearColor: 'black', + clearAlpha: 1.0, + viewOffsetX: 0, + autoRotate: true, +}; + +init(); + +clearGui(); + +function clearGui() { + if (gui) gui.destroy(); + + gui = new GUI(); + + gui.add(params, 'sampleLevel', { + 'Level 0: 1 Sample': 0, + 'Level 1: 2 Samples': 1, + 'Level 2: 4 Samples': 2, + 'Level 3: 8 Samples': 3, + 'Level 4: 16 Samples': 4, + 'Level 5: 32 Samples': 5, + }); + gui.add(params, 'clearColor', ['black', 'white', 'blue', 'green', 'red']); + gui.add(params, 'clearAlpha', 0, 1); + gui.add(params, 'viewOffsetX', -100, 100); + gui.add(params, 'autoRotate'); + + gui.open(); +} + +function init() { + const width = window.innerWidth; + const height = window.innerHeight; + + renderer = new THREE.WebGPURenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + stats = new Stats(); + document.body.appendChild(stats.dom); + + timer = new Timer(); + + camera = new THREE.PerspectiveCamera(65, width / height, 3, 10); + camera.position.z = 7; + camera.setViewOffset(width, height, params.viewOffsetX, 0, width, height); + + scene = new THREE.Scene(); + + const group = new THREE.Group(); + scene.add(group); + + const light = new THREE.PointLight(0xefffef, 500); + light.position.z = 10; + light.position.y = -10; + light.position.x = -10; + scene.add(light); + + const light2 = new THREE.PointLight(0xffefef, 500); + light2.position.z = 10; + light2.position.x = -10; + light2.position.y = 10; + scene.add(light2); + + const light3 = new THREE.PointLight(0xefefff, 500); + light3.position.z = 10; + light3.position.x = 10; + light3.position.y = -10; + scene.add(light3); + + const light4 = new THREE.AmbientLight(0xffffff, 0.2); + scene.add(light4); + + const geometry = new THREE.SphereGeometry(3, 48, 24); + const material = new THREE.MeshStandardMaterial(); + + mesh = new THREE.InstancedMesh(geometry, material, 120); + + const dummy = new THREE.Mesh(); + const color = new THREE.Color(); + + for (let i = 0; i < mesh.count; i++) { + dummy.position.x = Math.random() * 4 - 2; + dummy.position.y = Math.random() * 4 - 2; + dummy.position.z = Math.random() * 4 - 2; + dummy.rotation.x = Math.random(); + dummy.rotation.y = Math.random(); + dummy.rotation.z = Math.random(); + dummy.scale.setScalar(Math.random() * 0.2 + 0.05); + + dummy.updateMatrix(); + + color.setHSL(Math.random(), 1.0, 0.3); + + mesh.setMatrixAt(i, dummy.matrix); + mesh.setColorAt(i, color); + } + + scene.add(mesh); + + // postprocessing + + postProcessing = new THREE.PostProcessing(renderer); + + ssaaRenderPass = ssaaPass(scene, camera); + const scenePassColor = ssaaRenderPass.getTextureNode(); + + postProcessing.outputNode = scenePassColor; + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.setViewOffset(width, height, params.viewOffsetX, 0, width, height); + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); +} + +function animate() { + timer.update(); + + if (params.autoRotate) { + const delta = timer.getDelta(); + + mesh.rotation.x += delta * 0.25; + mesh.rotation.y += delta * 0.5; + } + + let newColor = ssaaRenderPass.clearColor; + + switch (params.clearColor) { + case 'blue': + newColor = 0x0000ff; + break; + case 'red': + newColor = 0xff0000; + break; + case 'green': + newColor = 0x00ff00; + break; + case 'white': + newColor = 0xffffff; + break; + case 'black': + newColor = 0x000000; + break; + } + + ssaaRenderPass.clearColor.set(newColor); + ssaaRenderPass.clearAlpha = params.clearAlpha; + + ssaaRenderPass.sampleLevel = params.sampleLevel; + + camera.view.offsetX = params.viewOffsetX; + + postProcessing.render(); + + stats.update(); +} diff --git a/examples-testing/examples/webgpu_postprocessing_transition.ts b/examples-testing/examples/webgpu_postprocessing_transition.ts new file mode 100644 index 000000000..b66bad12c --- /dev/null +++ b/examples-testing/examples/webgpu_postprocessing_transition.ts @@ -0,0 +1,200 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import TWEEN from 'three/addons/libs/tween.module.js'; +import { uniform, transition, pass } from 'three/tsl'; + +let renderer, postProcessing, transitionController, transitionPass; + +const textures = []; +const clock = new THREE.Clock(); + +const effectController = { + animateScene: true, + animateTransition: true, + transition: 0, + _transition: uniform(0), + useTexture: true, + _useTexture: uniform(1), + texture: 5, + cycle: true, + threshold: uniform(0.1), +}; + +function generateInstancedMesh(geometry, material, count) { + const mesh = new THREE.InstancedMesh(geometry, material, count); + + const dummy = new THREE.Object3D(); + const color = new THREE.Color(); + + for (let i = 0; i < count; i++) { + dummy.position.x = Math.random() * 100 - 50; + dummy.position.y = Math.random() * 60 - 30; + dummy.position.z = Math.random() * 80 - 40; + + dummy.rotation.x = Math.random() * 2 * Math.PI; + dummy.rotation.y = Math.random() * 2 * Math.PI; + dummy.rotation.z = Math.random() * 2 * Math.PI; + + dummy.scale.x = Math.random() * 2 + 1; + + if (geometry.type === 'BoxGeometry') { + dummy.scale.y = Math.random() * 2 + 1; + dummy.scale.z = Math.random() * 2 + 1; + } else { + dummy.scale.y = dummy.scale.x; + dummy.scale.z = dummy.scale.x; + } + + dummy.updateMatrix(); + + mesh.setMatrixAt(i, dummy.matrix); + mesh.setColorAt(i, color.setScalar(0.1 + 0.9 * Math.random())); + } + + return mesh; +} + +function FXScene(geometry, rotationSpeed, backgroundColor) { + const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.z = 20; + + // Setup scene + const scene = new THREE.Scene(); + scene.background = new THREE.Color(backgroundColor); + scene.add(new THREE.AmbientLight(0xaaaaaa, 3)); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0, 1, 4); + scene.add(light); + + this.rotationSpeed = rotationSpeed; + + const color = geometry.type === 'BoxGeometry' ? 0x0000ff : 0xff0000; + const material = new THREE.MeshPhongNodeMaterial({ color: color, flatShading: true }); + const mesh = generateInstancedMesh(geometry, material, 500); + scene.add(mesh); + + this.scene = scene; + this.camera = camera; + this.mesh = mesh; + + this.update = function (delta) { + if (effectController.animateScene) { + mesh.rotation.x += this.rotationSpeed.x * delta; + mesh.rotation.y += this.rotationSpeed.y * delta; + mesh.rotation.z += this.rotationSpeed.z * delta; + } + }; + + this.resize = function () { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + }; +} + +const fxSceneA = new FXScene(new THREE.BoxGeometry(2, 2, 2), new THREE.Vector3(0, -0.4, 0), 0xffffff); +const fxSceneB = new FXScene(new THREE.IcosahedronGeometry(1, 1), new THREE.Vector3(0, 0.2, 0.1), 0x000000); + +function init() { + // Initialize textures + + const loader = new THREE.TextureLoader(); + + for (let i = 0; i < 6; i++) { + textures[i] = loader.load('textures/transition/transition' + (i + 1) + '.png'); + } + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + postProcessing = new THREE.PostProcessing(renderer); + + const scenePassA = pass(fxSceneA.scene, fxSceneA.camera); + const scenePassB = pass(fxSceneB.scene, fxSceneB.camera); + + transitionPass = transition( + scenePassA, + scenePassB, + new THREE.TextureNode(textures[effectController.texture]), + effectController._transition, + effectController.threshold, + effectController._useTexture, + ); + + postProcessing.outputNode = transitionPass; + + const gui = new GUI(); + + gui.add(effectController, 'animateScene').name('Animate Scene'); + gui.add(effectController, 'animateTransition').name('Animate Transition'); + transitionController = gui + .add(effectController, 'transition', 0, 1, 0.01) + .name('transition') + .onChange(() => { + effectController._transition.value = effectController.transition; + }); + gui.add(effectController, 'useTexture').onChange(() => { + const value = effectController.useTexture ? 1 : 0; + effectController._useTexture.value = value; + }); + gui.add(effectController, 'texture', { Perlin: 0, Squares: 1, Cells: 2, Distort: 3, Gradient: 4, Radial: 5 }); + gui.add(effectController, 'cycle'); + gui.add(effectController.threshold, 'value', 0, 1, 0.01).name('threshold'); +} + +window.addEventListener('resize', onWindowResize); + +function onWindowResize() { + fxSceneA.resize(); + fxSceneB.resize(); + renderer.setSize(window.innerWidth, window.innerHeight); +} + +new TWEEN.Tween(effectController) + .to({ transition: 1 }, 1500) + .onUpdate(function () { + transitionController.setValue(effectController.transition); + + // Change the current alpha texture after each transition + if (effectController.cycle) { + if (effectController.transition == 0 || effectController.transition == 1) { + effectController.texture = (effectController.texture + 1) % textures.length; + } + } + }) + .repeat(Infinity) + .delay(2000) + .yoyo(true) + .start(); + +function animate() { + if (effectController.animateTransition) TWEEN.update(); + + if (textures[effectController.texture]) { + const mixTexture = textures[effectController.texture]; + transitionPass.mixTextureNode.value = mixTexture; + } + + const delta = clock.getDelta(); + fxSceneA.update(delta); + fxSceneB.update(delta); + + render(); +} + +function render() { + // Prevent render both scenes when it's not necessary + if (effectController.transition === 0) { + renderer.render(fxSceneB.scene, fxSceneB.camera); + } else if (effectController.transition === 1) { + renderer.render(fxSceneA.scene, fxSceneA.camera); + } else { + postProcessing.render(); + } +} + +init(); diff --git a/examples-testing/examples/webgpu_procedural_texture.ts b/examples-testing/examples/webgpu_procedural_texture.ts new file mode 100644 index 000000000..d4228f908 --- /dev/null +++ b/examples-testing/examples/webgpu_procedural_texture.ts @@ -0,0 +1,74 @@ +import * as THREE from 'three'; +import { checker, uv, uniform } from 'three/tsl'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; + +let camera, scene, renderer; + +init(); +render(); + +function init() { + const aspect = window.innerWidth / window.innerHeight; + camera = new THREE.OrthographicCamera(-aspect, aspect, 1, -1, 0, 2); + camera.position.z = 1; + + scene = new THREE.Scene(); + + // procedural to texture + + const uvScale = uniform(4); + const blurAmount = uniform(0.5); + + const procedural = checker(uv().mul(uvScale)); + const proceduralToTexture = procedural.toTexture(512, 512); // ( width, height ) <- texture size + + const colorNode = proceduralToTexture.gaussianBlur(blurAmount, 10); + + // extra + + //proceduralToTexture.autoUpdate = false; // update just once + //proceduralToTexture.textureNeedsUpdate = true; // manually update + + // scene + + const material = new THREE.MeshBasicNodeMaterial(); + material.colorNode = colorNode; + + const plane = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material); + scene.add(plane); + + // renderer + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(render); + document.body.appendChild(renderer.domElement); + + window.addEventListener('resize', onWindowResize); + + // gui + + const gui = new GUI(); + gui.add(uvScale, 'value', 1, 10).name('uv scale ( before rtt )'); + gui.add(blurAmount, 'value', 0, 2).name('blur amount ( after rtt )'); + gui.add(proceduralToTexture, 'autoUpdate').name('auto update'); +} + +function onWindowResize() { + renderer.setSize(window.innerWidth, window.innerHeight); + + const aspect = window.innerWidth / window.innerHeight; + + const frustumHeight = camera.top - camera.bottom; + + camera.left = (-frustumHeight * aspect) / 2; + camera.right = (frustumHeight * aspect) / 2; + + camera.updateProjectionMatrix(); +} + +function render() { + renderer.renderAsync(scene, camera); +} diff --git a/examples-testing/examples/webgpu_refraction.ts b/examples-testing/examples/webgpu_refraction.ts new file mode 100644 index 000000000..bf36d9119 --- /dev/null +++ b/examples-testing/examples/webgpu_refraction.ts @@ -0,0 +1,141 @@ +import * as THREE from 'three'; +import { viewportSafeUV, viewportSharedTexture, viewportTopLeft, texture, uv } from 'three/tsl'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer; + +let cameraControls; + +let smallSphere; + +init(); + +function init() { + // scene + scene = new THREE.Scene(); + + // camera + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 500); + camera.position.set(0, 75, 160); + + // + + const geometry = new THREE.IcosahedronGeometry(5, 0); + const material = new THREE.MeshPhongMaterial({ color: 0xffffff, emissive: 0x7b7b7b, flatShading: true }); + smallSphere = new THREE.Mesh(geometry, material); + scene.add(smallSphere); + + // textures + + const loader = new THREE.TextureLoader(); + + const floorNormal = loader.load('textures/floors/FloorsCheckerboard_S_Normal.jpg'); + floorNormal.wrapS = THREE.RepeatWrapping; + floorNormal.wrapT = THREE.RepeatWrapping; + + // refractor + + const verticalNormalScale = 0.1; + const verticalUVOffset = texture(floorNormal, uv().mul(5)).xy.mul(2).sub(1).mul(verticalNormalScale); + + const refractorUV = viewportTopLeft.add(verticalUVOffset); + const verticalRefractor = viewportSharedTexture(viewportSafeUV(refractorUV)); + + const planeGeo = new THREE.PlaneGeometry(100.1, 100.1); + + const planeRefractor = new THREE.Mesh( + planeGeo, + new THREE.MeshBasicNodeMaterial({ + backdropNode: verticalRefractor, + }), + ); + planeRefractor.material.transparent = true; + planeRefractor.position.y = 50; + scene.add(planeRefractor); + + // walls + + const planeTop = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0xffffff })); + planeTop.position.y = 100; + planeTop.rotateX(Math.PI / 2); + scene.add(planeTop); + + const planeBottom = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0xffffff })); + planeBottom.rotateX(-Math.PI / 2); + scene.add(planeBottom); + + const planeBack = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0x7f7fff })); + planeBack.position.z = -50; + planeBack.position.y = 50; + scene.add(planeBack); + + const planeRight = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0x00ff00 })); + planeRight.position.x = 50; + planeRight.position.y = 50; + planeRight.rotateY(-Math.PI / 2); + scene.add(planeRight); + + const planeLeft = new THREE.Mesh(planeGeo, new THREE.MeshPhongMaterial({ color: 0xff0000 })); + planeLeft.position.x = -50; + planeLeft.position.y = 50; + planeLeft.rotateY(Math.PI / 2); + scene.add(planeLeft); + + // lights + + const mainLight = new THREE.PointLight(0xe7e7e7, 2.5, 250, 0); + mainLight.position.y = 60; + scene.add(mainLight); + + const greenLight = new THREE.PointLight(0x00ff00, 0.5, 1000, 0); + greenLight.position.set(550, 50, 0); + scene.add(greenLight); + + const redLight = new THREE.PointLight(0xff0000, 0.5, 1000, 0); + redLight.position.set(-550, 50, 0); + scene.add(redLight); + + const blueLight = new THREE.PointLight(0xbbbbfe, 0.5, 1000, 0); + blueLight.position.set(0, 50, 550); + scene.add(blueLight); + + // renderer + + renderer = new THREE.WebGPURenderer(/*{ antialias: true }*/); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // controls + + cameraControls = new OrbitControls(camera, renderer.domElement); + cameraControls.target.set(0, 40, 0); + cameraControls.maxDistance = 400; + cameraControls.minDistance = 10; + cameraControls.update(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const timer = Date.now() * 0.01; + + smallSphere.position.set( + Math.cos(timer * 0.1) * 30, + Math.abs(Math.cos(timer * 0.2)) * 20 + 5, + Math.sin(timer * 0.1) * 30, + ); + smallSphere.rotation.y = Math.PI / 2 - timer * 0.1; + smallSphere.rotation.z = timer * 0.8; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_sky.ts b/examples-testing/examples/webgpu_sky.ts new file mode 100644 index 000000000..097d06af6 --- /dev/null +++ b/examples-testing/examples/webgpu_sky.ts @@ -0,0 +1,95 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { SkyMesh } from 'three/addons/objects/SkyMesh.js'; + +let camera, scene, renderer; + +let sky, sun; + +init(); + +function initSky() { + // Add Sky + sky = new SkyMesh(); + sky.scale.setScalar(450000); + scene.add(sky); + + sun = new THREE.Vector3(); + + /// GUI + + const effectController = { + turbidity: 10, + rayleigh: 3, + mieCoefficient: 0.005, + mieDirectionalG: 0.7, + elevation: 2, + azimuth: 180, + exposure: renderer.toneMappingExposure, + }; + + function guiChanged() { + sky.turbidity.value = effectController.turbidity; + sky.rayleigh.value = effectController.rayleigh; + sky.mieCoefficient.value = effectController.mieCoefficient; + sky.mieDirectionalG.value = effectController.mieDirectionalG; + + const phi = THREE.MathUtils.degToRad(90 - effectController.elevation); + const theta = THREE.MathUtils.degToRad(effectController.azimuth); + + sun.setFromSphericalCoords(1, phi, theta); + + sky.sunPosition.value.copy(sun); + + renderer.toneMappingExposure = effectController.exposure; + } + + const gui = new GUI(); + + gui.add(effectController, 'turbidity', 0.0, 20.0, 0.1).onChange(guiChanged); + gui.add(effectController, 'rayleigh', 0.0, 4, 0.001).onChange(guiChanged); + gui.add(effectController, 'mieCoefficient', 0.0, 0.1, 0.001).onChange(guiChanged); + gui.add(effectController, 'mieDirectionalG', 0.0, 1, 0.001).onChange(guiChanged); + gui.add(effectController, 'elevation', 0, 90, 0.1).onChange(guiChanged); + gui.add(effectController, 'azimuth', -180, 180, 0.1).onChange(guiChanged); + gui.add(effectController, 'exposure', 0, 1, 0.0001).onChange(guiChanged); + + guiChanged(); +} + +function init() { + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 100, 2000000); + camera.position.set(0, 100, 2000); + + scene = new THREE.Scene(); + + renderer = new THREE.WebGPURenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 0.5; + document.body.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + //controls.maxPolarAngle = Math.PI / 2; + controls.enableZoom = false; + controls.enablePan = false; + + initSky(); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_textures_anisotropy.ts b/examples-testing/examples/webgpu_textures_anisotropy.ts new file mode 100644 index 000000000..7eb0ce1b3 --- /dev/null +++ b/examples-testing/examples/webgpu_textures_anisotropy.ts @@ -0,0 +1,155 @@ +import * as THREE from 'three'; + +import Stats from 'three/addons/libs/stats.module.js'; + +let container, stats; + +let camera, scene1, scene2, renderer; + +let mouseX = 0, + mouseY = 0; + +init(); + +function init() { + const SCREEN_WIDTH = window.innerWidth; + const SCREEN_HEIGHT = window.innerHeight; + + container = document.createElement('div'); + document.body.appendChild(container); + + renderer = new THREE.WebGPURenderer({ antialias: true, forceWebGL: false }); + + // RENDERER + + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); + renderer.setAnimationLoop(animate); + renderer.autoClear = false; + + renderer.domElement.style.position = 'relative'; + container.appendChild(renderer.domElement); + + // + + camera = new THREE.PerspectiveCamera(35, SCREEN_WIDTH / SCREEN_HEIGHT, 1, 25000); + camera.position.z = 1500; + + scene1 = new THREE.Scene(); + scene1.fog = new THREE.Fog(0xf2f7ff, 1, 25000); + + scene2 = new THREE.Scene(); + scene2.fog = new THREE.Fog(0xf2f7ff, 1, 25000); + + scene1.add(new THREE.AmbientLight(0xeef0ff, 3)); + scene2.add(new THREE.AmbientLight(0xeef0ff, 3)); + + const light1 = new THREE.DirectionalLight(0xffffff, 6); + light1.position.set(1, 1, 1); + scene1.add(light1); + + const light2 = new THREE.DirectionalLight(0xffffff, 6); + light2.position.set(1, 1, 1); + scene2.add(light2); + + // GROUND + + const textureLoader = new THREE.TextureLoader(); + + const maxAnisotropy = renderer.getMaxAnisotropy(); + + const texture1 = textureLoader.load('textures/crate.gif'); + const material1 = new THREE.MeshPhongMaterial({ color: 0xffffff, map: texture1 }); + + texture1.colorSpace = THREE.SRGBColorSpace; + texture1.anisotropy = renderer.getMaxAnisotropy(); + texture1.wrapS = texture1.wrapT = THREE.RepeatWrapping; + texture1.repeat.set(512, 512); + + const texture2 = textureLoader.load('textures/crate.gif'); + const material2 = new THREE.MeshPhongMaterial({ color: 0xffffff, map: texture2 }); + + texture2.colorSpace = THREE.SRGBColorSpace; + texture2.anisotropy = 1; + texture2.wrapS = texture2.wrapT = THREE.RepeatWrapping; + texture2.repeat.set(512, 512); + + if (maxAnisotropy > 0) { + document.getElementById('val_left').innerHTML = texture1.anisotropy; + document.getElementById('val_right').innerHTML = texture2.anisotropy; + } else { + document.getElementById('val_left').innerHTML = 'not supported'; + document.getElementById('val_right').innerHTML = 'not supported'; + } + + // + + const geometry = new THREE.PlaneGeometry(100, 100); + + const mesh1 = new THREE.Mesh(geometry, material1); + mesh1.rotation.x = -Math.PI / 2; + mesh1.scale.set(1000, 1000, 1000); + + const mesh2 = new THREE.Mesh(geometry, material2); + mesh2.rotation.x = -Math.PI / 2; + mesh2.scale.set(1000, 1000, 1000); + + scene1.add(mesh1); + scene2.add(mesh2); + + // STATS1 + + stats = new Stats(); + container.appendChild(stats.dom); + + document.addEventListener('mousemove', onDocumentMouseMove); + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onDocumentMouseMove(event) { + const windowHalfX = window.innerWidth / 2; + const windowHalfY = window.innerHeight / 2; + + mouseX = event.clientX - windowHalfX; + mouseY = event.clientY - windowHalfY; +} + +function animate() { + render(); + stats.update(); +} + +function render() { + const SCREEN_WIDTH = window.innerWidth; + const SCREEN_HEIGHT = window.innerHeight; + + camera.position.x += (mouseX - camera.position.x) * 0.05; + camera.position.y = THREE.MathUtils.clamp( + camera.position.y + (-(mouseY - 200) - camera.position.y) * 0.05, + 50, + 1000, + ); + + camera.lookAt(scene1.position); + renderer.clear(); + + renderer.setScissorTest(true); + + renderer.setScissor(0, 0, SCREEN_WIDTH / 2 - 2, SCREEN_HEIGHT); + renderer.render(scene1, camera); + + renderer.setScissorTest(true); + + renderer.setScissor(SCREEN_WIDTH / 2, 0, SCREEN_WIDTH / 2 - 2, SCREEN_HEIGHT); + renderer.render(scene2, camera); + + renderer.setScissorTest(false); +} diff --git a/examples-testing/examples/webgpu_textures_partialupdate.ts b/examples-testing/examples/webgpu_textures_partialupdate.ts new file mode 100644 index 000000000..e8ebe87db --- /dev/null +++ b/examples-testing/examples/webgpu_textures_partialupdate.ts @@ -0,0 +1,103 @@ +import * as THREE from 'three'; + +let camera, scene, renderer, clock, dataTexture, diffuseMap; + +let last = 0; +const position = new THREE.Vector2(); +const color = new THREE.Color(); + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10); + camera.position.z = 2; + + scene = new THREE.Scene(); + + clock = new THREE.Clock(); + + const loader = new THREE.TextureLoader(); + diffuseMap = loader.load('textures/carbon/Carbon.png', animate); + diffuseMap.colorSpace = THREE.SRGBColorSpace; + diffuseMap.minFilter = THREE.LinearFilter; + diffuseMap.generateMipmaps = false; + + const geometry = new THREE.PlaneGeometry(2, 2); + const material = new THREE.MeshBasicMaterial({ map: diffuseMap }); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // + + const width = 32; + const height = 32; + + const data = new Uint8Array(width * height * 4); + dataTexture = new THREE.DataTexture(data, width, height); + + // + + renderer = new THREE.WebGPURenderer({ antialias: true, forceWebGL: false }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + + document.body.appendChild(renderer.domElement); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +async function animate() { + requestAnimationFrame(animate); + + const elapsedTime = clock.getElapsedTime(); + + await renderer.renderAsync(scene, camera); + + if (elapsedTime - last > 0.1) { + last = elapsedTime; + + position.x = 32 * THREE.MathUtils.randInt(1, 16) - 32; + position.y = 32 * THREE.MathUtils.randInt(1, 16) - 32; + + // generate new color data + updateDataTexture(dataTexture); + + // perform copy from src to dest texture to a random position + + renderer.copyTextureToTexture(dataTexture, diffuseMap, null, position); + } +} + +function updateDataTexture(texture) { + const size = texture.image.width * texture.image.height; + const data = texture.image.data; + + // generate a random color and update texture data + + color.setHex(Math.random() * 0xffffff); + + const r = Math.floor(color.r * 255); + const g = Math.floor(color.g * 255); + const b = Math.floor(color.b * 255); + + for (let i = 0; i < size; i++) { + const stride = i * 4; + + data[stride] = r; + data[stride + 1] = g; + data[stride + 2] = b; + data[stride + 3] = 1; + } + + texture.needsUpdate = true; +} diff --git a/examples-testing/examples/webgpu_tsl_coffee_smoke.ts b/examples-testing/examples/webgpu_tsl_coffee_smoke.ts new file mode 100644 index 000000000..506f14f19 --- /dev/null +++ b/examples-testing/examples/webgpu_tsl_coffee_smoke.ts @@ -0,0 +1,132 @@ +import * as THREE from 'three'; +import { mix, mul, positionLocal, smoothstep, texture, timerLocal, Fn, uv, vec2, vec3, vec4 } from 'three/tsl'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +let camera, scene, renderer, controls; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(25, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(8, 10, 12); + + scene = new THREE.Scene(); + + // Loaders + + const gltfLoader = new GLTFLoader(); + const textureLoader = new THREE.TextureLoader(); + + // baked model + + gltfLoader.load('./models/gltf/coffeeMug.glb', gltf => { + gltf.scene.getObjectByName('baked').material.map.anisotropy = 8; + scene.add(gltf.scene); + }); + + // geometry + + const smokeGeometry = new THREE.PlaneGeometry(1, 1, 16, 64); + smokeGeometry.translate(0, 0.5, 0); + smokeGeometry.scale(1.5, 6, 1.5); + + // texture + + const noiseTexture = textureLoader.load('./textures/noises/perlin/128x128.png'); + noiseTexture.wrapS = THREE.RepeatWrapping; + noiseTexture.wrapT = THREE.RepeatWrapping; + + // material + + const smokeMaterial = new THREE.MeshBasicNodeMaterial({ + transparent: true, + side: THREE.DoubleSide, + depthWrite: false, + }); + const time = timerLocal(); + + // position + + smokeMaterial.positionNode = Fn(() => { + // twist + + const twistNoiseUv = vec2(0.5, uv().y.mul(0.2).sub(time.mul(0.005)).mod(1)); + const twist = texture(noiseTexture, twistNoiseUv).r.mul(10); + positionLocal.xz.assign(positionLocal.xz.rotateUV(twist, vec2(0))); + + // wind + + const windOffset = vec2( + texture(noiseTexture, vec2(0.25, time.mul(0.01)).mod(1)).r.sub(0.5), + texture(noiseTexture, vec2(0.75, time.mul(0.01)).mod(1)).r.sub(0.5), + ).mul(uv().y.pow(2).mul(10)); + positionLocal.addAssign(windOffset); + + return positionLocal; + })(); + + // color + + smokeMaterial.colorNode = Fn(() => { + // alpha + + const alphaNoiseUv = uv() + .mul(vec2(0.5, 0.3)) + .add(vec2(0, time.mul(0.03).negate())); + const alpha = mul( + // pattern + texture(noiseTexture, alphaNoiseUv).r.smoothstep(0.4, 1), + + // edges fade + smoothstep(0, 0.1, uv().x), + smoothstep(1, 0.9, uv().x), + smoothstep(0, 0.1, uv().y), + smoothstep(1, 0.9, uv().y), + ); + + // color + + const finalColor = mix(vec3(0.6, 0.3, 0.2), vec3(1, 1, 1), alpha.pow(3)); + + return vec4(finalColor, alpha); + })(); + + // mesh + + const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial); + smoke.position.y = 1.83; + scene.add(smoke); + + // renderer + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // controls + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.minDistance = 0.1; + controls.maxDistance = 50; + controls.target.y = 3; + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +async function animate() { + controls.update(); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_tsl_vfx_flames.ts b/examples-testing/examples/webgpu_tsl_vfx_flames.ts new file mode 100644 index 000000000..a9110fa8c --- /dev/null +++ b/examples-testing/examples/webgpu_tsl_vfx_flames.ts @@ -0,0 +1,203 @@ +import * as THREE from 'three'; +import { + PI2, + spherizeUV, + sin, + step, + texture, + timerLocal, + Fn, + uv, + vec2, + vec3, + vec4, + mix, + billboarding, +} from 'three/tsl'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer, controls; + +init(); + +function init() { + camera = new THREE.PerspectiveCamera(25, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(1, 1, 3); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x201919); + + // textures + + const textureLoader = new THREE.TextureLoader(); + + const cellularTexture = textureLoader.load('./textures/noises/voronoi/grayscale-256x256.png'); + const perlinTexture = textureLoader.load('./textures/noises/perlin/rgb-256x256.png'); + + // gradient canvas + + const gradient = {}; + gradient.element = document.createElement('canvas'); + gradient.element.width = 128; + gradient.element.height = 1; + gradient.context = gradient.element.getContext('2d'); + + gradient.colors = ['#090033', '#5f1f93', '#e02e96', '#ffbd80', '#fff0db']; + + gradient.texture = new THREE.CanvasTexture(gradient.element); + gradient.texture.colorSpace = THREE.SRGBColorSpace; + + gradient.update = () => { + const fillGradient = gradient.context.createLinearGradient(0, 0, gradient.element.width, 0); + + for (let i = 0; i < gradient.colors.length; i++) { + const progress = i / (gradient.colors.length - 1); + const color = gradient.colors[i]; + fillGradient.addColorStop(progress, color); + } + + gradient.context.fillStyle = fillGradient; + gradient.context.fillRect(0, 0, gradient.element.width, gradient.element.height); + + gradient.texture.needsUpdate = true; + }; + + gradient.update(); + + // flame 1 material + + const flame1Material = new THREE.SpriteNodeMaterial({ transparent: true, side: THREE.DoubleSide }); + + flame1Material.colorNode = Fn(() => { + const time = timerLocal(); + + // main UV + const mainUv = uv().toVar(); + mainUv.assign(spherizeUV(mainUv, 10).mul(0.6).add(0.2)); // spherize + mainUv.assign(mainUv.pow(vec2(1, 2))); // stretch + mainUv.assign(mainUv.mul(2, 1).sub(vec2(0.5, 0))); // scale + + // gradients + const gradient1 = sin(time.mul(10).sub(mainUv.y.mul(PI2).mul(2))).toVar(); + const gradient2 = mainUv.y.smoothstep(0, 1).toVar(); + mainUv.x.addAssign(gradient1.mul(gradient2).mul(0.2)); + + // cellular noise + const cellularUv = mainUv + .mul(0.5) + .add(vec2(0, time.negate().mul(0.5))) + .mod(1); + const cellularNoise = texture(cellularTexture, cellularUv, 0).r.oneMinus().smoothstep(0, 0.5).oneMinus(); + cellularNoise.mulAssign(gradient2); + + // shape + const shape = mainUv.sub(0.5).mul(vec2(3, 2)).length().oneMinus().toVar(); + shape.assign(shape.sub(cellularNoise)); + + // gradient color + const gradientColor = texture(gradient.texture, vec2(shape.remap(0, 1, 0, 1), 0)); + + // output + const color = mix(gradientColor, vec3(1), shape.step(0.8).oneMinus()); + const alpha = shape.smoothstep(0, 0.3); + return vec4(color.rgb, alpha); + })(); + + // flame 2 material + + const flame2Material = new THREE.SpriteNodeMaterial({ transparent: true, side: THREE.DoubleSide }); + + flame2Material.colorNode = Fn(() => { + const time = timerLocal(); + + // main UV + const mainUv = uv().toVar(); + mainUv.assign(spherizeUV(mainUv, 10).mul(0.6).add(0.2)); // spherize + mainUv.assign(mainUv.pow(vec2(1, 3))); // stretch + mainUv.assign(mainUv.mul(2, 1).sub(vec2(0.5, 0))); // scale + + // perlin noise + const perlinUv = mainUv.add(vec2(0, time.negate().mul(1))).mod(1); + const perlinNoise = texture(perlinTexture, perlinUv, 0).sub(0.5).mul(1); + mainUv.x.addAssign(perlinNoise.x.mul(0.5)); + + // gradients + const gradient1 = sin(time.mul(10).sub(mainUv.y.mul(PI2).mul(2))); + const gradient2 = mainUv.y.smoothstep(0, 1); + const gradient3 = mainUv.y.smoothstep(1, 0.7); + mainUv.x.addAssign(gradient1.mul(gradient2).mul(0.2)); + + // displaced perlin noise + const displacementPerlinUv = mainUv + .mul(0.5) + .add(vec2(0, time.negate().mul(0.25))) + .mod(1); + const displacementPerlinNoise = texture(perlinTexture, displacementPerlinUv, 0).sub(0.5).mul(1); + const displacedPerlinUv = mainUv + .add(vec2(0, time.negate().mul(0.5))) + .add(displacementPerlinNoise) + .mod(1); + const displacedPerlinNoise = texture(perlinTexture, displacedPerlinUv, 0).sub(0.5).mul(1); + mainUv.x.addAssign(displacedPerlinNoise.mul(0.5)); + + // cellular noise + const cellularUv = mainUv.add(vec2(0, time.negate().mul(1.5))).mod(1); + const cellularNoise = texture(cellularTexture, cellularUv, 0).r.oneMinus().smoothstep(0.25, 1); + + // shape + const shape = mainUv.sub(0.5).mul(vec2(6, 1)).length().step(0.5); + shape.assign(shape.mul(cellularNoise)); + shape.mulAssign(gradient3); + shape.assign(step(0.01, shape)); + + // output + return vec4(vec3(1), shape); + })(); + + // billboarding - follow the camera rotation only horizontally + + flame1Material.vertexNode = billboarding(); + flame2Material.vertexNode = billboarding(); + + // meshes + + const flame1 = new THREE.Sprite(flame1Material); + flame1.center.set(0.5, 0); + flame1.scale.x = 0.5; // optional + flame1.position.x = -0.5; + scene.add(flame1); + + const flame2 = new THREE.Sprite(flame2Material); + flame2.center.set(0.5, 0); + flame2.position.x = 0.5; + scene.add(flame2); + + // renderer + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.minDistance = 0.1; + controls.maxDistance = 50; + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +async function animate() { + controls.update(); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_video_panorama.ts b/examples-testing/examples/webgpu_video_panorama.ts new file mode 100644 index 000000000..e409b3c07 --- /dev/null +++ b/examples-testing/examples/webgpu_video_panorama.ts @@ -0,0 +1,99 @@ +import * as THREE from 'three'; + +let camera, scene, renderer; + +let isUserInteracting = false, + lon = 0, + lat = 0, + phi = 0, + theta = 0, + onPointerDownPointerX = 0, + onPointerDownPointerY = 0, + onPointerDownLon = 0, + onPointerDownLat = 0; + +const distance = 0.5; + +init(); + +function init() { + const container = document.getElementById('container'); + + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.25, 10); + + scene = new THREE.Scene(); + + const geometry = new THREE.SphereGeometry(5, 60, 40); + // invert the geometry on the x-axis so that all of the faces point inward + geometry.scale(-1, 1, 1); + + const video = document.getElementById('video'); + video.play(); + + const texture = new THREE.VideoTexture(video); + texture.colorSpace = THREE.SRGBColorSpace; + const material = new THREE.MeshBasicMaterial({ map: texture }); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + container.appendChild(renderer.domElement); + + document.addEventListener('pointerdown', onPointerDown); + document.addEventListener('pointermove', onPointerMove); + document.addEventListener('pointerup', onPointerUp); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onPointerDown(event) { + isUserInteracting = true; + + onPointerDownPointerX = event.clientX; + onPointerDownPointerY = event.clientY; + + onPointerDownLon = lon; + onPointerDownLat = lat; +} + +function onPointerMove(event) { + if (isUserInteracting === true) { + lon = (onPointerDownPointerX - event.clientX) * 0.1 + onPointerDownLon; + lat = (onPointerDownPointerY - event.clientY) * 0.1 + onPointerDownLat; + } +} + +function onPointerUp() { + isUserInteracting = false; +} + +function animate() { + update(); +} + +function update() { + lat = Math.max(-85, Math.min(85, lat)); + phi = THREE.MathUtils.degToRad(90 - lat); + theta = THREE.MathUtils.degToRad(lon); + + camera.position.x = distance * Math.sin(phi) * Math.cos(theta); + camera.position.y = distance * Math.cos(phi); + camera.position.z = distance * Math.sin(phi) * Math.sin(theta); + + camera.lookAt(0, 0, 0); + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webgpu_water.ts b/examples-testing/examples/webgpu_water.ts new file mode 100644 index 000000000..76e09f1f8 --- /dev/null +++ b/examples-testing/examples/webgpu_water.ts @@ -0,0 +1,171 @@ +import * as THREE from 'three'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { WaterMesh } from 'three/addons/objects/Water2Mesh.js'; + +let scene, camera, clock, renderer, water; + +let torusKnot; + +const params = { + color: '#ffffff', + scale: 4, + flowX: 1, + flowY: 1, +}; + +init(); + +function init() { + // scene + + scene = new THREE.Scene(); + + // camera + + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200); + camera.position.set(-15, 7, 15); + camera.lookAt(scene.position); + + // clock + + clock = new THREE.Clock(); + + // mesh + + const torusKnotGeometry = new THREE.TorusKnotGeometry(3, 1, 256, 32); + const torusKnotMaterial = new THREE.MeshNormalMaterial(); + + torusKnot = new THREE.Mesh(torusKnotGeometry, torusKnotMaterial); + torusKnot.position.y = 4; + torusKnot.scale.set(0.5, 0.5, 0.5); + scene.add(torusKnot); + + // ground + + const groundGeometry = new THREE.PlaneGeometry(20, 20); + const groundMaterial = new THREE.MeshStandardMaterial({ roughness: 0.8, metalness: 0.4 }); + const ground = new THREE.Mesh(groundGeometry, groundMaterial); + ground.rotation.x = Math.PI * -0.5; + scene.add(ground); + + const textureLoader = new THREE.TextureLoader(); + textureLoader.load('textures/hardwood2_diffuse.jpg', function (map) { + map.wrapS = THREE.RepeatWrapping; + map.wrapT = THREE.RepeatWrapping; + map.anisotropy = 16; + map.repeat.set(4, 4); + map.colorSpace = THREE.SRGBColorSpace; + groundMaterial.map = map; + groundMaterial.needsUpdate = true; + }); + + // + + const normalMap0 = textureLoader.load('textures/water/Water_1_M_Normal.jpg'); + const normalMap1 = textureLoader.load('textures/water/Water_2_M_Normal.jpg'); + + normalMap0.wrapS = normalMap0.wrapT = THREE.RepeatWrapping; + normalMap1.wrapS = normalMap1.wrapT = THREE.RepeatWrapping; + + // water + + const waterGeometry = new THREE.PlaneGeometry(20, 20); + + water = new WaterMesh(waterGeometry, { + color: params.color, + scale: params.scale, + flowDirection: new THREE.Vector2(params.flowX, params.flowY), + normalMap0: normalMap0, + normalMap1: normalMap1, + }); + + water.position.y = 1; + water.rotation.x = Math.PI * -0.5; + scene.add(water); + + // skybox + + const cubeTextureLoader = new THREE.CubeTextureLoader(); + cubeTextureLoader.setPath('textures/cube/Park2/'); + + const cubeTexture = cubeTextureLoader.load([ + 'posx.jpg', + 'negx.jpg', + 'posy.jpg', + 'negy.jpg', + 'posz.jpg', + 'negz.jpg', + ]); + + scene.background = cubeTexture; + + // light + + const ambientLight = new THREE.AmbientLight(0xe7e7e7, 1.2); + scene.add(ambientLight); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 2); + directionalLight.position.set(-1, 1, 1); + scene.add(directionalLight); + + // renderer + + renderer = new THREE.WebGPURenderer(); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setAnimationLoop(animate); + document.body.appendChild(renderer.domElement); + + // gui + + const gui = new GUI(); + const waterNode = water.material.fragmentNode; + + gui.addColor(params, 'color').onChange(function (value) { + waterNode.color.value.set(value); + }); + gui.add(params, 'scale', 1, 10).onChange(function (value) { + waterNode.scale.value = value; + }); + gui.add(params, 'flowX', -1, 1) + .step(0.01) + .onChange(function (value) { + waterNode.flowDirection.value.x = value; + waterNode.flowDirection.value.normalize(); + }); + gui.add(params, 'flowY', -1, 1) + .step(0.01) + .onChange(function (value) { + waterNode.flowDirection.value.y = value; + waterNode.flowDirection.value.normalize(); + }); + + gui.open(); + + // + + const controls = new OrbitControls(camera, renderer.domElement); + controls.minDistance = 5; + controls.maxDistance = 50; + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const delta = clock.getDelta(); + + torusKnot.rotation.x += delta; + torusKnot.rotation.y += delta * 0.5; + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webxr_ar_cones.ts b/examples-testing/examples/webxr_ar_cones.ts new file mode 100644 index 000000000..95eb34393 --- /dev/null +++ b/examples-testing/examples/webxr_ar_cones.ts @@ -0,0 +1,66 @@ +import * as THREE from 'three'; +import { ARButton } from 'three/addons/webxr/ARButton.js'; + +let camera, scene, renderer; +let controller; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 20); + + const light = new THREE.HemisphereLight(0xffffff, 0xbbbbff, 3); + light.position.set(0.5, 1, 0.25); + scene.add(light); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.xr.enabled = true; + container.appendChild(renderer.domElement); + + // + + document.body.appendChild(ARButton.createButton(renderer)); + + // + + const geometry = new THREE.CylinderGeometry(0, 0.05, 0.2, 32).rotateX(Math.PI / 2); + + function onSelect() { + const material = new THREE.MeshPhongMaterial({ color: 0xffffff * Math.random() }); + const mesh = new THREE.Mesh(geometry, material); + mesh.position.set(0, 0, -0.3).applyMatrix4(controller.matrixWorld); + mesh.quaternion.setFromRotationMatrix(controller.matrixWorld); + scene.add(mesh); + } + + controller = renderer.xr.getController(0); + controller.addEventListener('select', onSelect); + scene.add(controller); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webxr_ar_hittest.ts b/examples-testing/examples/webxr_ar_hittest.ts new file mode 100644 index 000000000..1867cc470 --- /dev/null +++ b/examples-testing/examples/webxr_ar_hittest.ts @@ -0,0 +1,115 @@ +import * as THREE from 'three'; +import { ARButton } from 'three/addons/webxr/ARButton.js'; + +let container; +let camera, scene, renderer; +let controller; + +let reticle; + +let hitTestSource = null; +let hitTestSourceRequested = false; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 20); + + const light = new THREE.HemisphereLight(0xffffff, 0xbbbbff, 3); + light.position.set(0.5, 1, 0.25); + scene.add(light); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.xr.enabled = true; + container.appendChild(renderer.domElement); + + // + + document.body.appendChild(ARButton.createButton(renderer, { requiredFeatures: ['hit-test'] })); + + // + + const geometry = new THREE.CylinderGeometry(0.1, 0.1, 0.2, 32).translate(0, 0.1, 0); + + function onSelect() { + if (reticle.visible) { + const material = new THREE.MeshPhongMaterial({ color: 0xffffff * Math.random() }); + const mesh = new THREE.Mesh(geometry, material); + reticle.matrix.decompose(mesh.position, mesh.quaternion, mesh.scale); + mesh.scale.y = Math.random() * 2 + 1; + scene.add(mesh); + } + } + + controller = renderer.xr.getController(0); + controller.addEventListener('select', onSelect); + scene.add(controller); + + reticle = new THREE.Mesh( + new THREE.RingGeometry(0.15, 0.2, 32).rotateX(-Math.PI / 2), + new THREE.MeshBasicMaterial(), + ); + reticle.matrixAutoUpdate = false; + reticle.visible = false; + scene.add(reticle); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate(timestamp, frame) { + if (frame) { + const referenceSpace = renderer.xr.getReferenceSpace(); + const session = renderer.xr.getSession(); + + if (hitTestSourceRequested === false) { + session.requestReferenceSpace('viewer').then(function (referenceSpace) { + session.requestHitTestSource({ space: referenceSpace }).then(function (source) { + hitTestSource = source; + }); + }); + + session.addEventListener('end', function () { + hitTestSourceRequested = false; + hitTestSource = null; + }); + + hitTestSourceRequested = true; + } + + if (hitTestSource) { + const hitTestResults = frame.getHitTestResults(hitTestSource); + + if (hitTestResults.length) { + const hit = hitTestResults[0]; + + reticle.visible = true; + reticle.matrix.fromArray(hit.getPose(referenceSpace).transform.matrix); + } else { + reticle.visible = false; + } + } + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webxr_ar_lighting.ts b/examples-testing/examples/webxr_ar_lighting.ts new file mode 100644 index 000000000..9de23ad94 --- /dev/null +++ b/examples-testing/examples/webxr_ar_lighting.ts @@ -0,0 +1,124 @@ +import * as THREE from 'three'; +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; +import { ARButton } from 'three/addons/webxr/ARButton.js'; +import { XREstimatedLight } from 'three/addons/webxr/XREstimatedLight.js'; + +let camera, scene, renderer; +let controller; +let defaultEnvironment; + +init(); + +function init() { + const container = document.createElement('div'); + document.body.appendChild(container); + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 20); + + const defaultLight = new THREE.HemisphereLight(0xffffff, 0xbbbbff, 1); + defaultLight.position.set(0.5, 1, 0.25); + scene.add(defaultLight); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.xr.enabled = true; + container.appendChild(renderer.domElement); + + // Don't add the XREstimatedLight to the scene initially. + // It doesn't have any estimated lighting values until an AR session starts. + + const xrLight = new XREstimatedLight(renderer); + + xrLight.addEventListener('estimationstart', () => { + // Swap the default light out for the estimated one one we start getting some estimated values. + scene.add(xrLight); + scene.remove(defaultLight); + + // The estimated lighting also provides an environment cubemap, which we can apply here. + if (xrLight.environment) { + scene.environment = xrLight.environment; + } + }); + + xrLight.addEventListener('estimationend', () => { + // Swap the lights back when we stop receiving estimated values. + scene.add(defaultLight); + scene.remove(xrLight); + + // Revert back to the default environment. + scene.environment = defaultEnvironment; + }); + + // + + new RGBELoader().setPath('textures/equirectangular/').load('royal_esplanade_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + defaultEnvironment = texture; + + scene.environment = defaultEnvironment; + }); + + // + + // In order for lighting estimation to work, 'light-estimation' must be included as either an optional or required feature. + document.body.appendChild(ARButton.createButton(renderer, { optionalFeatures: ['light-estimation'] })); + + // + + const ballGeometry = new THREE.SphereGeometry(0.175, 32, 32); + const ballGroup = new THREE.Group(); + ballGroup.position.z = -2; + + const rows = 3; + const cols = 3; + + for (let i = 0; i < rows; i++) { + for (let j = 0; j < cols; j++) { + const ballMaterial = new THREE.MeshStandardMaterial({ + color: 0xdddddd, + roughness: i / rows, + metalness: j / cols, + }); + const ballMesh = new THREE.Mesh(ballGeometry, ballMaterial); + ballMesh.position.set((i + 0.5 - rows * 0.5) * 0.4, (j + 0.5 - cols * 0.5) * 0.4, 0); + ballGroup.add(ballMesh); + } + } + + scene.add(ballGroup); + + // + + function onSelect() { + ballGroup.position.set(0, 0, -2).applyMatrix4(controller.matrixWorld); + ballGroup.quaternion.setFromRotationMatrix(controller.matrixWorld); + } + + controller = renderer.xr.getController(0); + controller.addEventListener('select', onSelect); + scene.add(controller); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webxr_ar_plane_detection.ts b/examples-testing/examples/webxr_ar_plane_detection.ts new file mode 100644 index 000000000..841b6b04b --- /dev/null +++ b/examples-testing/examples/webxr_ar_plane_detection.ts @@ -0,0 +1,46 @@ +import * as THREE from 'three'; +import { ARButton } from 'three/addons/webxr/ARButton.js'; +import { XRPlanes } from 'three/addons/webxr/XRPlanes.js'; + +// + +const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); +renderer.setPixelRatio(window.devicePixelRatio); +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.setAnimationLoop(animate); +renderer.xr.enabled = true; +document.body.appendChild(renderer.domElement); + +document.body.appendChild( + ARButton.createButton(renderer, { + requiredFeatures: ['plane-detection'], + }), +); + +window.addEventListener('resize', onWindowResize); + +// + +const scene = new THREE.Scene(); + +const planes = new XRPlanes(renderer); +scene.add(planes); + +const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 20); + +const light = new THREE.HemisphereLight(0xffffff, 0xbbbbff, 3); +light.position.set(0.5, 1, 0.25); +scene.add(light); + +// + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webxr_vr_handinput.ts b/examples-testing/examples/webxr_vr_handinput.ts new file mode 100644 index 000000000..d746e4582 --- /dev/null +++ b/examples-testing/examples/webxr_vr_handinput.ts @@ -0,0 +1,126 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { VRButton } from 'three/addons/webxr/VRButton.js'; +import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js'; +import { XRHandModelFactory } from 'three/addons/webxr/XRHandModelFactory.js'; + +let container; +let camera, scene, renderer; +let hand1, hand2; +let controller1, controller2; +let controllerGrip1, controllerGrip2; + +let controls; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x444444); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10); + camera.position.set(0, 1.6, 3); + + controls = new OrbitControls(camera, container); + controls.target.set(0, 1.6, 0); + controls.update(); + + const floorGeometry = new THREE.PlaneGeometry(4, 4); + const floorMaterial = new THREE.MeshStandardMaterial({ color: 0x666666 }); + const floor = new THREE.Mesh(floorGeometry, floorMaterial); + floor.rotation.x = -Math.PI / 2; + floor.receiveShadow = true; + scene.add(floor); + + scene.add(new THREE.HemisphereLight(0xbcbcbc, 0xa5a5a5, 3)); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0, 6, 0); + light.castShadow = true; + light.shadow.camera.top = 2; + light.shadow.camera.bottom = -2; + light.shadow.camera.right = 2; + light.shadow.camera.left = -2; + light.shadow.mapSize.set(4096, 4096); + scene.add(light); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + renderer.xr.enabled = true; + + container.appendChild(renderer.domElement); + + const sessionInit = { + requiredFeatures: ['hand-tracking'], + }; + + document.body.appendChild(VRButton.createButton(renderer, sessionInit)); + + // controllers + + controller1 = renderer.xr.getController(0); + scene.add(controller1); + + controller2 = renderer.xr.getController(1); + scene.add(controller2); + + const controllerModelFactory = new XRControllerModelFactory(); + const handModelFactory = new XRHandModelFactory(); + + // Hand 1 + controllerGrip1 = renderer.xr.getControllerGrip(0); + controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1)); + scene.add(controllerGrip1); + + hand1 = renderer.xr.getHand(0); + hand1.add(handModelFactory.createHandModel(hand1)); + + scene.add(hand1); + + // Hand 2 + controllerGrip2 = renderer.xr.getControllerGrip(1); + controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2)); + scene.add(controllerGrip2); + + hand2 = renderer.xr.getHand(1); + hand2.add(handModelFactory.createHandModel(hand2)); + scene.add(hand2); + + // + + const geometry = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(0, 0, 0), + new THREE.Vector3(0, 0, -1), + ]); + + const line = new THREE.Line(geometry); + line.name = 'line'; + line.scale.z = 5; + + controller1.add(line.clone()); + controller2.add(line.clone()); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} +// + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webxr_vr_panorama.ts b/examples-testing/examples/webxr_vr_panorama.ts new file mode 100644 index 000000000..535e1c937 --- /dev/null +++ b/examples-testing/examples/webxr_vr_panorama.ts @@ -0,0 +1,92 @@ +import * as THREE from 'three'; +import { VRButton } from 'three/addons/webxr/VRButton.js'; + +let camera; +let renderer; +let scene; + +init(); + +function init() { + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.xr.enabled = true; + renderer.xr.setReferenceSpaceType('local'); + document.body.appendChild(renderer.domElement); + + document.body.appendChild(VRButton.createButton(renderer)); + + // + + scene = new THREE.Scene(); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); + camera.layers.enable(1); + + const geometry = new THREE.BoxGeometry(100, 100, 100); + geometry.scale(1, 1, -1); + + const textures = getTexturesFromAtlasFile('textures/cube/sun_temple_stripe_stereo.jpg', 12); + + const materials = []; + + for (let i = 0; i < 6; i++) { + materials.push(new THREE.MeshBasicMaterial({ map: textures[i] })); + } + + const skyBox = new THREE.Mesh(geometry, materials); + skyBox.layers.set(1); + scene.add(skyBox); + + const materialsR = []; + + for (let i = 6; i < 12; i++) { + materialsR.push(new THREE.MeshBasicMaterial({ map: textures[i] })); + } + + const skyBoxR = new THREE.Mesh(geometry, materialsR); + skyBoxR.layers.set(2); + scene.add(skyBoxR); + + window.addEventListener('resize', onWindowResize); +} + +function getTexturesFromAtlasFile(atlasImgUrl, tilesNum) { + const textures = []; + + for (let i = 0; i < tilesNum; i++) { + textures[i] = new THREE.Texture(); + } + + const loader = new THREE.ImageLoader(); + loader.load(atlasImgUrl, function (imageObj) { + let canvas, context; + const tileWidth = imageObj.height; + + for (let i = 0; i < textures.length; i++) { + canvas = document.createElement('canvas'); + context = canvas.getContext('2d'); + canvas.height = tileWidth; + canvas.width = tileWidth; + context.drawImage(imageObj, tileWidth * i, 0, tileWidth, tileWidth, 0, 0, tileWidth, tileWidth); + textures[i].colorSpace = THREE.SRGBColorSpace; + textures[i].image = canvas; + textures[i].needsUpdate = true; + } + }); + + return textures; +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webxr_vr_panorama_depth.ts b/examples-testing/examples/webxr_vr_panorama_depth.ts new file mode 100644 index 000000000..66215469d --- /dev/null +++ b/examples-testing/examples/webxr_vr_panorama_depth.ts @@ -0,0 +1,90 @@ +import * as THREE from 'three'; +import { VRButton } from 'three/addons/webxr/VRButton.js'; + +let camera, scene, renderer, sphere, clock; + +init(); + +function init() { + const container = document.getElementById('container'); + + clock = new THREE.Clock(); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x101010); + + const light = new THREE.AmbientLight(0xffffff, 3); + scene.add(light); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 2000); + scene.add(camera); + + // Create the panoramic sphere geometery + const panoSphereGeo = new THREE.SphereGeometry(6, 256, 256); + + // Create the panoramic sphere material + const panoSphereMat = new THREE.MeshStandardMaterial({ + side: THREE.BackSide, + displacementScale: -4.0, + }); + + // Create the panoramic sphere mesh + sphere = new THREE.Mesh(panoSphereGeo, panoSphereMat); + + // Load and assign the texture and depth map + const manager = new THREE.LoadingManager(); + const loader = new THREE.TextureLoader(manager); + + loader.load('./textures/kandao3.jpg', function (texture) { + texture.colorSpace = THREE.SRGBColorSpace; + texture.minFilter = THREE.NearestFilter; + texture.generateMipmaps = false; + sphere.material.map = texture; + }); + + loader.load('./textures/kandao3_depthmap.jpg', function (depth) { + depth.minFilter = THREE.NearestFilter; + depth.generateMipmaps = false; + sphere.material.displacementMap = depth; + }); + + // On load complete add the panoramic sphere to the scene + manager.onLoad = function () { + scene.add(sphere); + }; + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.xr.enabled = true; + renderer.xr.setReferenceSpaceType('local'); + container.appendChild(renderer.domElement); + + document.body.appendChild(VRButton.createButton(renderer)); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + // If we are not presenting move the camera a little so the effect is visible + + if (renderer.xr.isPresenting === false) { + const time = clock.getElapsedTime(); + + sphere.rotation.y += 0.001; + sphere.position.x = Math.sin(time) * 0.2; + sphere.position.z = Math.cos(time) * 0.2; + } + + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webxr_vr_rollercoaster.ts b/examples-testing/examples/webxr_vr_rollercoaster.ts new file mode 100644 index 000000000..b8c35a9e3 --- /dev/null +++ b/examples-testing/examples/webxr_vr_rollercoaster.ts @@ -0,0 +1,211 @@ +import * as THREE from 'three'; + +import { + RollerCoasterGeometry, + RollerCoasterShadowGeometry, + RollerCoasterLiftersGeometry, + TreesGeometry, + SkyGeometry, +} from 'three/addons/misc/RollerCoaster.js'; +import { VRButton } from 'three/addons/webxr/VRButton.js'; + +let mesh, material, geometry; + +const renderer = new THREE.WebGLRenderer({ antialias: true }); +renderer.setPixelRatio(window.devicePixelRatio); +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.setAnimationLoop(animate); +renderer.xr.enabled = true; +renderer.xr.setReferenceSpaceType('local'); +document.body.appendChild(renderer.domElement); + +document.body.appendChild(VRButton.createButton(renderer)); + +// + +const scene = new THREE.Scene(); +scene.background = new THREE.Color(0xf0f0ff); + +const light = new THREE.HemisphereLight(0xfff0f0, 0x60606, 3); +light.position.set(1, 1, 1); +scene.add(light); + +const train = new THREE.Object3D(); +scene.add(train); + +const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 500); +train.add(camera); + +// environment + +geometry = new THREE.PlaneGeometry(500, 500, 15, 15); +geometry.rotateX(-Math.PI / 2); + +const positions = geometry.attributes.position.array; +const vertex = new THREE.Vector3(); + +for (let i = 0; i < positions.length; i += 3) { + vertex.fromArray(positions, i); + + vertex.x += Math.random() * 10 - 5; + vertex.z += Math.random() * 10 - 5; + + const distance = vertex.distanceTo(scene.position) / 5 - 25; + vertex.y = Math.random() * Math.max(0, distance); + + vertex.toArray(positions, i); +} + +geometry.computeVertexNormals(); + +material = new THREE.MeshLambertMaterial({ + color: 0x407000, +}); + +mesh = new THREE.Mesh(geometry, material); +scene.add(mesh); + +geometry = new TreesGeometry(mesh); +material = new THREE.MeshBasicMaterial({ + side: THREE.DoubleSide, + vertexColors: true, +}); +mesh = new THREE.Mesh(geometry, material); +scene.add(mesh); + +geometry = new SkyGeometry(); +material = new THREE.MeshBasicMaterial({ color: 0xffffff }); +mesh = new THREE.Mesh(geometry, material); +scene.add(mesh); + +// + +const PI2 = Math.PI * 2; + +const curve = (function () { + const vector = new THREE.Vector3(); + const vector2 = new THREE.Vector3(); + + return { + getPointAt: function (t) { + t = t * PI2; + + const x = Math.sin(t * 3) * Math.cos(t * 4) * 50; + const y = Math.sin(t * 10) * 2 + Math.cos(t * 17) * 2 + 5; + const z = Math.sin(t) * Math.sin(t * 4) * 50; + + return vector.set(x, y, z).multiplyScalar(2); + }, + + getTangentAt: function (t) { + const delta = 0.0001; + const t1 = Math.max(0, t - delta); + const t2 = Math.min(1, t + delta); + + return vector2.copy(this.getPointAt(t2)).sub(this.getPointAt(t1)).normalize(); + }, + }; +})(); + +geometry = new RollerCoasterGeometry(curve, 1500); +material = new THREE.MeshPhongMaterial({ + vertexColors: true, +}); +mesh = new THREE.Mesh(geometry, material); +scene.add(mesh); + +geometry = new RollerCoasterLiftersGeometry(curve, 100); +material = new THREE.MeshPhongMaterial(); +mesh = new THREE.Mesh(geometry, material); +mesh.position.y = 0.1; +scene.add(mesh); + +geometry = new RollerCoasterShadowGeometry(curve, 500); +material = new THREE.MeshBasicMaterial({ + color: 0x305000, + depthWrite: false, + transparent: true, +}); +mesh = new THREE.Mesh(geometry, material); +mesh.position.y = 0.1; +scene.add(mesh); + +const funfairs = []; + +// + +geometry = new THREE.CylinderGeometry(10, 10, 5, 15); +material = new THREE.MeshLambertMaterial({ + color: 0xff8080, +}); +mesh = new THREE.Mesh(geometry, material); +mesh.position.set(-80, 10, -70); +mesh.rotation.x = Math.PI / 2; +scene.add(mesh); + +funfairs.push(mesh); + +geometry = new THREE.CylinderGeometry(5, 6, 4, 10); +material = new THREE.MeshLambertMaterial({ + color: 0x8080ff, +}); +mesh = new THREE.Mesh(geometry, material); +mesh.position.set(50, 2, 30); +scene.add(mesh); + +funfairs.push(mesh); + +// + +window.addEventListener('resize', onWindowResize); + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +const position = new THREE.Vector3(); +const tangent = new THREE.Vector3(); + +const lookAt = new THREE.Vector3(); + +let velocity = 0; +let progress = 0; + +let prevTime = performance.now(); + +function animate() { + const time = performance.now(); + const delta = time - prevTime; + + for (let i = 0; i < funfairs.length; i++) { + funfairs[i].rotation.y = time * 0.0004; + } + + // + + progress += velocity; + progress = progress % 1; + + position.copy(curve.getPointAt(progress)); + position.y += 0.3; + + train.position.copy(position); + + tangent.copy(curve.getTangentAt(progress)); + + velocity -= tangent.y * 0.0000001 * delta; + velocity = Math.max(0.00004, Math.min(0.0002, velocity)); + + train.lookAt(lookAt.copy(position).sub(tangent)); + + // + + renderer.render(scene, camera); + + prevTime = time; +} diff --git a/examples-testing/examples/webxr_vr_sandbox.ts b/examples-testing/examples/webxr_vr_sandbox.ts new file mode 100644 index 000000000..9e8e75909 --- /dev/null +++ b/examples-testing/examples/webxr_vr_sandbox.ts @@ -0,0 +1,192 @@ +import * as THREE from 'three'; + +import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; +import { Reflector } from 'three/addons/objects/Reflector.js'; +import { VRButton } from 'three/addons/webxr/VRButton.js'; + +import { HTMLMesh } from 'three/addons/interactive/HTMLMesh.js'; +import { InteractiveGroup } from 'three/addons/interactive/InteractiveGroup.js'; +import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js'; + +import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; +import Stats from 'three/addons/libs/stats.module.js'; + +let camera, scene, renderer; +let reflector; +let stats, statsMesh; + +const parameters = { + radius: 0.6, + tube: 0.2, + tubularSegments: 150, + radialSegments: 20, + p: 2, + q: 3, + thickness: 0.5, +}; + +init(); + +function init() { + scene = new THREE.Scene(); + + new RGBELoader().setPath('textures/equirectangular/').load('moonless_golf_1k.hdr', function (texture) { + texture.mapping = THREE.EquirectangularReflectionMapping; + + scene.background = texture; + scene.environment = texture; + }); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10); + camera.position.set(0, 1.6, 1.5); + + // + + const torusGeometry = new THREE.TorusKnotGeometry(...Object.values(parameters)); + const torusMaterial = new THREE.MeshPhysicalMaterial({ + transmission: 1.0, + roughness: 0, + metalness: 0.25, + thickness: 0.5, + side: THREE.DoubleSide, + }); + const torus = new THREE.Mesh(torusGeometry, torusMaterial); + torus.name = 'torus'; + torus.position.y = 1.5; + torus.position.z = -2; + scene.add(torus); + + const cylinderGeometry = new THREE.CylinderGeometry(1, 1, 0.1, 50); + const cylinderMaterial = new THREE.MeshStandardMaterial(); + const cylinder = new THREE.Mesh(cylinderGeometry, cylinderMaterial); + cylinder.position.z = -2; + scene.add(cylinder); + + // + + reflector = new Reflector(new THREE.PlaneGeometry(2, 2), { + textureWidth: window.innerWidth, + textureHeight: window.innerHeight, + }); + reflector.position.x = 1; + reflector.position.y = 1.5; + reflector.position.z = -3; + reflector.rotation.y = -Math.PI / 4; + scene.add(reflector); + + const frameGeometry = new THREE.BoxGeometry(2.1, 2.1, 0.1); + const frameMaterial = new THREE.MeshPhongMaterial(); + const frame = new THREE.Mesh(frameGeometry, frameMaterial); + frame.position.z = -0.07; + reflector.add(frame); + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.autoClear = false; + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.xr.enabled = true; + renderer.toneMapping = THREE.ACESFilmicToneMapping; + renderer.toneMappingExposure = 1; + document.body.appendChild(renderer.domElement); + + document.body.appendChild(VRButton.createButton(renderer)); + + window.addEventListener('resize', onWindowResize); + + // + + const geometry = new THREE.BufferGeometry(); + geometry.setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -5)]); + + const controller1 = renderer.xr.getController(0); + controller1.add(new THREE.Line(geometry)); + scene.add(controller1); + + const controller2 = renderer.xr.getController(1); + controller2.add(new THREE.Line(geometry)); + scene.add(controller2); + + // + + const controllerModelFactory = new XRControllerModelFactory(); + + const controllerGrip1 = renderer.xr.getControllerGrip(0); + controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1)); + scene.add(controllerGrip1); + + const controllerGrip2 = renderer.xr.getControllerGrip(1); + controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2)); + scene.add(controllerGrip2); + + // GUI + + function onChange() { + torus.geometry.dispose(); + torus.geometry = new THREE.TorusKnotGeometry(...Object.values(parameters)); + } + + function onThicknessChange() { + torus.material.thickness = parameters.thickness; + } + + const gui = new GUI({ width: 300 }); + gui.add(parameters, 'radius', 0.0, 1.0).onChange(onChange); + gui.add(parameters, 'tube', 0.0, 1.0).onChange(onChange); + gui.add(parameters, 'tubularSegments', 10, 150, 1).onChange(onChange); + gui.add(parameters, 'radialSegments', 2, 20, 1).onChange(onChange); + gui.add(parameters, 'p', 1, 10, 1).onChange(onChange); + gui.add(parameters, 'q', 0, 10, 1).onChange(onChange); + gui.add(parameters, 'thickness', 0, 1).onChange(onThicknessChange); + gui.domElement.style.visibility = 'hidden'; + + const group = new InteractiveGroup(); + group.listenToPointerEvents(renderer, camera); + group.listenToXRControllerEvents(controller1); + group.listenToXRControllerEvents(controller2); + scene.add(group); + + const mesh = new HTMLMesh(gui.domElement); + mesh.position.x = -0.75; + mesh.position.y = 1.5; + mesh.position.z = -0.5; + mesh.rotation.y = Math.PI / 4; + mesh.scale.setScalar(2); + group.add(mesh); + + // Add stats.js + stats = new Stats(); + stats.dom.style.width = '80px'; + stats.dom.style.height = '48px'; + document.body.appendChild(stats.dom); + + statsMesh = new HTMLMesh(stats.dom); + statsMesh.position.x = -0.75; + statsMesh.position.y = 2; + statsMesh.position.z = -0.6; + statsMesh.rotation.y = Math.PI / 4; + statsMesh.scale.setScalar(2.5); + group.add(statsMesh); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + const time = performance.now() * 0.0002; + const torus = scene.getObjectByName('torus'); + torus.rotation.x = time * 0.4; + torus.rotation.y = time; + + renderer.render(scene, camera); + stats.update(); + + // Canvas elements doesn't trigger DOM updates, so we have to update the texture + statsMesh.material.map.update(); +} diff --git a/examples-testing/examples/webxr_vr_video.ts b/examples-testing/examples/webxr_vr_video.ts new file mode 100644 index 000000000..50a990412 --- /dev/null +++ b/examples-testing/examples/webxr_vr_video.ts @@ -0,0 +1,92 @@ +import * as THREE from 'three'; +import { VRButton } from 'three/addons/webxr/VRButton.js'; + +let camera, scene, renderer; + +init(); + +function init() { + const container = document.getElementById('container'); + container.addEventListener('click', function () { + video.play(); + }); + + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 2000); + camera.layers.enable(1); // render left view when no stereo available + + // video + + const video = document.getElementById('video'); + video.play(); + + const texture = new THREE.VideoTexture(video); + texture.colorSpace = THREE.SRGBColorSpace; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x101010); + + // left + + const geometry1 = new THREE.SphereGeometry(500, 60, 40); + // invert the geometry on the x-axis so that all of the faces point inward + geometry1.scale(-1, 1, 1); + + const uvs1 = geometry1.attributes.uv.array; + + for (let i = 0; i < uvs1.length; i += 2) { + uvs1[i] *= 0.5; + } + + const material1 = new THREE.MeshBasicMaterial({ map: texture }); + + const mesh1 = new THREE.Mesh(geometry1, material1); + mesh1.rotation.y = -Math.PI / 2; + mesh1.layers.set(1); // display in left eye only + scene.add(mesh1); + + // right + + const geometry2 = new THREE.SphereGeometry(500, 60, 40); + geometry2.scale(-1, 1, 1); + + const uvs2 = geometry2.attributes.uv.array; + + for (let i = 0; i < uvs2.length; i += 2) { + uvs2[i] *= 0.5; + uvs2[i] += 0.5; + } + + const material2 = new THREE.MeshBasicMaterial({ map: texture }); + + const mesh2 = new THREE.Mesh(geometry2, material2); + mesh2.rotation.y = -Math.PI / 2; + mesh2.layers.set(2); // display in right eye only + scene.add(mesh2); + + // + + renderer = new THREE.WebGLRenderer(); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.xr.enabled = true; + renderer.xr.setReferenceSpaceType('local'); + container.appendChild(renderer.domElement); + + document.body.appendChild(VRButton.createButton(renderer)); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webxr_xr_controls_transform.ts b/examples-testing/examples/webxr_xr_controls_transform.ts new file mode 100644 index 000000000..f3b4796e6 --- /dev/null +++ b/examples-testing/examples/webxr_xr_controls_transform.ts @@ -0,0 +1,210 @@ +import * as THREE from 'three'; +import { TransformControls } from 'three/addons/controls/TransformControls.js'; +import { XRButton } from 'three/addons/webxr/XRButton.js'; +import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js'; + +let container; +let camera, scene, renderer; +let controller1, controller2, line; +let controllerGrip1, controllerGrip2; + +let raycaster; + +let controls, group; + +init(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x808080); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10); + camera.position.set(0, 1.6, 0); + + const floorGeometry = new THREE.PlaneGeometry(6, 6); + const floorMaterial = new THREE.ShadowMaterial({ + opacity: 0.25, + blending: THREE.CustomBlending, + transparent: false, + }); + const floor = new THREE.Mesh(floorGeometry, floorMaterial); + floor.rotation.x = -Math.PI / 2; + floor.receiveShadow = true; + scene.add(floor); + + scene.add(new THREE.HemisphereLight(0xbcbcbc, 0xa5a5a5, 3)); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0, 6, 0); + light.castShadow = true; + light.shadow.camera.top = 3; + light.shadow.camera.bottom = -3; + light.shadow.camera.right = 3; + light.shadow.camera.left = -3; + light.shadow.mapSize.set(4096, 4096); + scene.add(light); + + group = new THREE.Group(); + scene.add(group); + + const geometries = [ + new THREE.BoxGeometry(0.2, 0.2, 0.2), + new THREE.ConeGeometry(0.2, 0.4, 64), + new THREE.CylinderGeometry(0.2, 0.2, 0.2, 64), + new THREE.IcosahedronGeometry(0.2, 8), + new THREE.TorusGeometry(0.2, 0.04, 64, 32), + ]; + + for (let i = 0; i < 16; i++) { + const geometry = geometries[Math.floor(Math.random() * geometries.length)]; + const material = new THREE.MeshStandardMaterial({ + color: Math.random() * 0xffffff, + roughness: 0.7, + metalness: 0.0, + }); + + const object = new THREE.Mesh(geometry, material); + + object.position.x = Math.random() - 0.5; + object.position.y = Math.random() * 2 + 0.5; + object.position.z = Math.random() - 2.5; + + object.rotation.x = Math.random() * 2 * Math.PI; + object.rotation.y = Math.random() * 2 * Math.PI; + object.rotation.z = Math.random() * 2 * Math.PI; + + object.scale.setScalar(Math.random() + 0.5); + + object.castShadow = true; + object.receiveShadow = true; + + group.add(object); + } + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setAnimationLoop(animate); + renderer.shadowMap.enabled = true; + renderer.xr.enabled = true; + container.appendChild(renderer.domElement); + + document.body.appendChild(XRButton.createButton(renderer)); + + // controllers + + controller1 = renderer.xr.getController(0); + controller1.addEventListener('select', onSelect); + controller1.addEventListener('selectstart', onControllerEvent); + controller1.addEventListener('selectend', onControllerEvent); + controller1.addEventListener('move', onControllerEvent); + controller1.userData.active = false; + scene.add(controller1); + + controller2 = renderer.xr.getController(1); + controller2.addEventListener('select', onSelect); + controller2.addEventListener('selectstart', onControllerEvent); + controller2.addEventListener('selectend', onControllerEvent); + controller2.addEventListener('move', onControllerEvent); + controller2.userData.active = true; + scene.add(controller2); + + const controllerModelFactory = new XRControllerModelFactory(); + + controllerGrip1 = renderer.xr.getControllerGrip(0); + controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1)); + scene.add(controllerGrip1); + + controllerGrip2 = renderer.xr.getControllerGrip(1); + controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2)); + scene.add(controllerGrip2); + + // + + const geometry = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(0, 0, 0), + new THREE.Vector3(0, 0, -1), + ]); + + line = new THREE.Line(geometry); + line.name = 'line'; + line.scale.z = 5; + + raycaster = new THREE.Raycaster(); + + // controls + + controls = new TransformControls(camera, renderer.domElement); + controls.attach(group.children[0]); + scene.add(controls); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onSelect(event) { + const controller = event.target; + + controller1.userData.active = false; + controller2.userData.active = false; + + if (controller === controller1) { + controller1.userData.active = true; + controller1.add(line); + } + + if (controller === controller2) { + controller2.userData.active = true; + controller2.add(line); + } + + raycaster.setFromXRController(controller); + + const intersects = raycaster.intersectObjects(group.children); + + if (intersects.length > 0) { + controls.attach(intersects[0].object); + } +} + +function onControllerEvent(event) { + const controller = event.target; + + if (controller.userData.active === false) return; + + controls.getRaycaster().setFromXRController(controller); + + switch (event.type) { + case 'selectstart': + controls.pointerDown(null); + break; + + case 'selectend': + controls.pointerUp(null); + break; + + case 'move': + controls.pointerHover(null); + controls.pointerMove(null); + break; + } +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// + +function animate() { + renderer.render(scene, camera); +} diff --git a/examples-testing/examples/webxr_xr_dragging_custom_depth.ts b/examples-testing/examples/webxr_xr_dragging_custom_depth.ts new file mode 100644 index 000000000..2cd50ba4c --- /dev/null +++ b/examples-testing/examples/webxr_xr_dragging_custom_depth.ts @@ -0,0 +1,395 @@ +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { XRButton } from 'three/addons/webxr/XRButton.js'; +import { XRControllerModelFactory } from 'three/addons/webxr/XRControllerModelFactory.js'; + +let container; +let camera, scene, renderer; +let controller1, controller2; +let controllerGrip1, controllerGrip2; +let isDepthSupplied = false; + +let raycaster; + +const intersected = []; +const tempMatrix = new THREE.Matrix4(); + +let controls, group; + +init(); +animate(); + +function init() { + container = document.createElement('div'); + document.body.appendChild(container); + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x808080); + + camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10); + camera.position.set(0, 1.6, 3); + + controls = new OrbitControls(camera, container); + controls.target.set(0, 1.6, 0); + controls.update(); + + const floorGeometry = new THREE.PlaneGeometry(6, 6); + const floorMaterial = new THREE.ShadowMaterial({ + opacity: 0.25, + blending: THREE.CustomBlending, + transparent: false, + }); + const floor = new THREE.Mesh(floorGeometry, floorMaterial); + floor.rotation.x = -Math.PI / 2; + floor.receiveShadow = true; + scene.add(floor); + + scene.add(new THREE.HemisphereLight(0xbcbcbc, 0xa5a5a5, 3)); + + const light = new THREE.DirectionalLight(0xffffff, 3); + light.position.set(0, 6, 0); + light.castShadow = true; + light.shadow.camera.top = 3; + light.shadow.camera.bottom = -3; + light.shadow.camera.right = 3; + light.shadow.camera.left = -3; + light.shadow.mapSize.set(4096, 4096); + scene.add(light); + + group = new THREE.Group(); + scene.add(group); + + const geometries = [ + new THREE.BoxGeometry(0.2, 0.2, 0.2), + new THREE.ConeGeometry(0.2, 0.2, 64), + new THREE.CylinderGeometry(0.2, 0.2, 0.2, 64), + new THREE.IcosahedronGeometry(0.2, 8), + new THREE.TorusGeometry(0.2, 0.04, 64, 32), + ]; + + for (let i = 0; i < 50; i++) { + const geometry = geometries[Math.floor(Math.random() * geometries.length)]; + const material = new THREE.ShaderMaterial({ + vertexShader: /* glsl */ ` + varying vec3 vNormal; + varying vec2 vUv; + void main() { + vNormal = normalize(normalMatrix * normal); + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + + fragmentShader: /* glsl */ ` + uniform vec3 diffuseColor; + uniform float roughness; + uniform float metalness; + uniform float emissive; + varying vec3 vNormal; + varying vec2 vUv; + uniform sampler2DArray depthColor; + uniform float depthWidth; + uniform float depthHeight; + #define saturate( a ) clamp( a, 0.0, 1.0 ) + float Depth_GetCameraDepthInMeters(const sampler2DArray depthTexture, + const vec2 depthUv, int arrayIndex) { + return texture(depthColor, vec3(depthUv.x, depthUv.y, arrayIndex)).r; + } + float Depth_GetOcclusion(const sampler2DArray depthTexture, const vec2 depthUv, float assetDepthM, int arrayIndex) { + float depthMm = Depth_GetCameraDepthInMeters(depthTexture, depthUv, arrayIndex); + const float kDepthTolerancePerM = 0.001; + return clamp(1.0 - + 0.5 * (depthMm - assetDepthM) / + (kDepthTolerancePerM * assetDepthM) + + 0.5, 0.0, 1.0); + } + float Depth_GetBlurredOcclusionAroundUV(const sampler2DArray depthTexture, const vec2 uv, float assetDepthM, int arrayIndex) { + // Kernel used: + // 0 4 7 4 0 + // 4 16 26 16 4 + // 7 26 41 26 7 + // 4 16 26 16 4 + // 0 4 7 4 0 + const float kKernelTotalWeights = 269.0; + float sum = 0.0; + const float kOcclusionBlurAmount = 0.0005; + vec2 blurriness = + vec2(kOcclusionBlurAmount, kOcclusionBlurAmount /** u_DepthAspectRatio*/); + float current = 0.0; + current += Depth_GetOcclusion( + depthTexture, uv + vec2(-1.0, -2.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(+1.0, -2.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(-1.0, +2.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(+1.0, +2.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(-2.0, +1.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(+2.0, +1.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(-2.0, -1.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(+2.0, -1.0) * blurriness, assetDepthM, arrayIndex); + sum += current * 4.0; + current = 0.0; + current += Depth_GetOcclusion( + depthTexture, uv + vec2(-2.0, -0.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(+2.0, +0.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(+0.0, +2.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(-0.0, -2.0) * blurriness, assetDepthM, arrayIndex); + sum += current * 7.0; + current = 0.0; + current += Depth_GetOcclusion( + depthTexture, uv + vec2(-1.0, -1.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(+1.0, -1.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(-1.0, +1.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(+1.0, +1.0) * blurriness, assetDepthM, arrayIndex); + sum += current * 16.0; + current = 0.0; + current += Depth_GetOcclusion( + depthTexture, uv + vec2(+0.0, +1.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(-0.0, -1.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(-1.0, -0.0) * blurriness, assetDepthM, arrayIndex); + current += Depth_GetOcclusion( + depthTexture, uv + vec2(+1.0, +0.0) * blurriness, assetDepthM, arrayIndex); + sum += current * 26.0; + sum += Depth_GetOcclusion(depthTexture, uv, assetDepthM, arrayIndex) * 41.0; + return sum / kKernelTotalWeights; + } + void main() { + vec3 normal = normalize(vNormal); + vec3 diffuse = diffuseColor; + float specularIntensity = pow(max(dot(normal, normalize(vec3(0, 0, 1))), 0.0), 64.0); + vec3 specular = vec3(specularIntensity) * mix(vec3(0.04), diffuse, metalness); + gl_FragColor = vec4(diffuse * (1.0 - specular) + specular, 1.0) * (1.0 + emissive); + + if (depthWidth > 0.0) { + int arrayIndex = 0; + vec2 depthUv; + if (gl_FragCoord.x>=depthWidth) { + arrayIndex = 1; + depthUv = vec2((gl_FragCoord.x-depthWidth)/depthWidth, gl_FragCoord.y/depthHeight); + } else { + depthUv = vec2(gl_FragCoord.x/depthWidth, gl_FragCoord.y/depthHeight); + } + float assetDepthM = gl_FragCoord.z; + + float occlusion = Depth_GetBlurredOcclusionAroundUV(depthColor, depthUv, assetDepthM, arrayIndex); + float depthMm = Depth_GetCameraDepthInMeters(depthColor, depthUv, arrayIndex); + + float absDistance = abs(assetDepthM - depthMm); + float v = 0.0025; + absDistance = saturate(v - absDistance) / v; + + gl_FragColor.rgb += vec3(absDistance * 2.0, absDistance * 2.0, absDistance * 12.0); + gl_FragColor = mix(gl_FragColor, vec4(0.0, 0.0, 0.0, 0.0), occlusion * 0.7); + } + } + `, + + uniforms: { + diffuseColor: { value: new THREE.Color(Math.random() * 0xffffff) }, + roughness: { value: 0.7 }, + metalness: { value: 0.0 }, + emissive: { value: 0.0 }, + depthWidth: { value: 0.0 }, + depthHeight: { value: 0.0 }, + depthColor: { value: new THREE.Texture() }, + }, + }); + + const object = new THREE.Mesh(geometry, material); + + object.position.x = Math.random() * 4 - 2; + object.position.y = Math.random() * 2; + object.position.z = Math.random() * 4 - 2; + + object.rotation.x = Math.random() * 2 * Math.PI; + object.rotation.y = Math.random() * 2 * Math.PI; + object.rotation.z = Math.random() * 2 * Math.PI; + + object.scale.setScalar(Math.random() + 0.5); + + group.add(object); + } + + // + + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.shadowMap.enabled = true; + renderer.xr.enabled = true; + container.appendChild(renderer.domElement); + + document.body.appendChild( + XRButton.createButton(renderer, { + optionalFeatures: ['depth-sensing'], + depthSensing: { usagePreference: ['gpu-optimized'], dataFormatPreference: [] }, + }), + ); + + // controllers + + controller1 = renderer.xr.getController(0); + controller1.addEventListener('selectstart', onSelectStart); + controller1.addEventListener('selectend', onSelectEnd); + scene.add(controller1); + + controller2 = renderer.xr.getController(1); + controller2.addEventListener('selectstart', onSelectStart); + controller2.addEventListener('selectend', onSelectEnd); + scene.add(controller2); + + const controllerModelFactory = new XRControllerModelFactory(); + + controllerGrip1 = renderer.xr.getControllerGrip(0); + controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1)); + scene.add(controllerGrip1); + + controllerGrip2 = renderer.xr.getControllerGrip(1); + controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2)); + scene.add(controllerGrip2); + + // + + const geometry = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(0, 0, 0), + new THREE.Vector3(0, 0, -1), + ]); + + const line = new THREE.Line(geometry); + line.name = 'line'; + line.scale.z = 5; + + controller1.add(line.clone()); + controller2.add(line.clone()); + + raycaster = new THREE.Raycaster(); + + // + + window.addEventListener('resize', onWindowResize); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function onSelectStart(event) { + const controller = event.target; + + const intersections = getIntersections(controller); + + if (intersections.length > 0) { + const intersection = intersections[0]; + + const object = intersection.object; + object.material.uniforms.emissive.value = 1; + controller.attach(object); + + controller.userData.selected = object; + } + + controller.userData.targetRayMode = event.data.targetRayMode; +} + +function onSelectEnd(event) { + const controller = event.target; + + if (controller.userData.selected !== undefined) { + const object = controller.userData.selected; + object.material.uniforms.emissive.value = 0; + group.attach(object); + + controller.userData.selected = undefined; + } +} + +function getIntersections(controller) { + controller.updateMatrixWorld(); + + tempMatrix.identity().extractRotation(controller.matrixWorld); + + raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld); + raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix); + + return raycaster.intersectObjects(group.children, false); +} + +function intersectObjects(controller) { + // Do not highlight in mobile-ar + + if (controller.userData.targetRayMode === 'screen') return; + + // Do not highlight when already selected + + if (controller.userData.selected !== undefined) return; + + const line = controller.getObjectByName('line'); + const intersections = getIntersections(controller); + + if (intersections.length > 0) { + const intersection = intersections[0]; + + const object = intersection.object; + object.material.uniforms.emissive.value = 1; + intersected.push(object); + + line.scale.z = intersection.distance; + } else { + line.scale.z = 5; + } +} + +function cleanIntersected() { + while (intersected.length) { + const object = intersected.pop(); + object.material.uniforms.emissive.value = 0; + } +} + +// + +function animate() { + renderer.setAnimationLoop(render); +} + +function render() { + if (renderer.xr.hasDepthSensing() && !isDepthSupplied) { + group.children.forEach(child => { + child.material.uniforms.depthColor.value = renderer.xr.getDepthTexture(); + child.material.uniforms.depthWidth.value = 1680; + child.material.uniforms.depthHeight.value = 1760; + + isDepthSupplied = true; + }); + } else if (!renderer.xr.hasDepthSensing() && isDepthSupplied) { + group.children.forEach(child => { + child.material.uniforms.depthWidth.value = 0; + child.material.uniforms.depthHeight.value = 0; + + isDepthSupplied = false; + }); + } + + cleanIntersected(); + + intersectObjects(controller1); + intersectObjects(controller2); + + renderer.render(scene, camera); +}