├── .gitattributes ├── .gitignore ├── CursorNode.js ├── MonkeyPaint.js ├── README.md ├── ScenePainter.js ├── ThreeQuery.js ├── UnwrapUVs.js ├── assets └── cursor.png ├── index.html ├── monkeh.blend ├── monkeh.blend1 ├── monkeh.glb ├── style.css └── three.module.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /art 2 | -------------------------------------------------------------------------------- /CursorNode.js: -------------------------------------------------------------------------------- 1 | import*as THREE from 'three'; 2 | 3 | 4 | class ScreenSpaceNode { 5 | constructor(camera, renderer) { 6 | this.camera = camera; 7 | this.renderer = renderer; 8 | this.plane = new THREE.Mesh(new THREE.PlaneGeometry(.01,.01),new THREE.MeshBasicMaterial({ 9 | color: 0xffffff, 10 | transparent: true, 11 | opacity: 1., 12 | side: THREE.DoubleSide 13 | })); 14 | let loader = new THREE.TextureLoader().load("./assets/cursor.png", (tex)=>{ 15 | this.plane.material.map = tex; 16 | this.plane.scale.x = 1; 17 | this.plane.scale.y = tex.image.height / tex.image.width; 18 | this.plane.scale.multiplyScalar(.5) 19 | } 20 | ) 21 | this.plane.geometry.translate(.0034, -0.004, 0) 22 | this.camera.add(this.plane); 23 | this.addEventListeners(); 24 | 25 | } 26 | } 27 | 28 | import $3 from "./ThreeQuery.js" 29 | 30 | function scaleObjectToScreenScale(object, camera) { 31 | // Assuming `object` is the object you want to scale 32 | 33 | // Get the camera's field of view in radians 34 | const fov = camera.fov * (Math.PI / 180); 35 | 36 | // Calculate the distance between the camera and the object 37 | const distance = camera.position.distanceTo(object.position); 38 | 39 | // Calculate the height of the object at the current distance 40 | const height = 2 * Math.tan(fov / 2) * distance; 41 | 42 | // Calculate the scaling factor to maintain the object's relative size 43 | const scaleFactor = height * .5; 44 | 45 | // Apply the scaling factor to the object 46 | object.scale.set(scaleFactor, scaleFactor, scaleFactor); 47 | } 48 | 49 | export default class CursorNode extends ScreenSpaceNode { 50 | constructor(camera, renderer) { 51 | super(camera, renderer) 52 | 53 | this.addEventListeners(); 54 | 55 | this.mouse = new THREE.Vector2(); 56 | this.fragMouse = new THREE.Vector2(); 57 | this.groundPoint = new THREE.Vector3(); 58 | 59 | 60 | 61 | //camera.parent.add(this.hitPlane) 62 | const raycaster = new THREE.Raycaster(); 63 | 64 | this.raycast=(object)=>{ 65 | 66 | raycaster.setFromCamera(this.mouse, this.camera); 67 | return raycaster.intersectObjects([object], true); 68 | } 69 | 70 | return 71 | 72 | this.marker = $3("").e[0]; 73 | this.marker.material = this.marker.material.clone(); 74 | //[this.marker.material.clone(),this.marker.material.clone()]; 75 | let sz = .5; 76 | this.marker.geometry.scale(sz, sz, sz) 77 | this.marker.material.transparent = true; 78 | this.marker.material.color.set('green') 79 | this.marker.material.opacity = .5; 80 | this.marker.material.blending = THREE.AdditiveBlending; 81 | let c = this.marker.clone(); 82 | this.marker.material.map = new THREE.TextureLoader().load("./assets/crosshair.png", (tex)=>{ 83 | this.marker.material.map = tex; 84 | 85 | c.material = this.marker.material.clone(); 86 | c.material.depthFunc = THREE.GreaterDepth; 87 | c.material.color.set('red') 88 | c.material.opacity = .5; 89 | 90 | } 91 | ) 92 | this.marker.add(c) 93 | this.marker.rotation.x = -Math.PI * .5; 94 | this.camera.parent.add(this.marker); 95 | 96 | 97 | this.hitPlane = new THREE.Mesh(new THREE.PlaneGeometry()); 98 | this.hitPlane.scale.set(10000, 10000, 10000); 99 | this.hitPlane.rotation.x = -Math.PI * .5; 100 | this.hitPlane.updateMatrix(); 101 | this.hitPlane.updateMatrixWorld(); 102 | 103 | this.plane.onBeforeRender = ()=>{ 104 | this.hitPlane.position.set(this.camera.position.x, 1, this.camera.position.z); 105 | this.hitPlane.updateMatrixWorld(); 106 | const intersects = this.raycast(this.hitPlane); 107 | if (intersects.length > 0) { 108 | this.marker.position.copy(intersects[0].point) 109 | this.marker.position.y += .01; 110 | if (!this.marker.geometry.boundingBox) 111 | this.marker.geometry.computeBoundingBox(); 112 | scaleObjectToScreenScale(this.marker, this.camera) 113 | } 114 | } 115 | } 116 | 117 | updateCursorPosition(event) { 118 | const rect = this.renderer.domElement.getBoundingClientRect(); 119 | let x = (event.clientX - rect.left); 120 | let y = (event.clientY - rect.top); 121 | this.fragMouse.set(x,y); 122 | x/=rect.width; 123 | y/=rect.height; 124 | this.fragMouse.y = rect.height-this.fragMouse.y; 125 | this.mouse.set(x * 2 - 1, -y * 2 + 1); 126 | this.plane.position.set(this.mouse.x, this.mouse.y, this.camera.near).unproject(this.camera); 127 | this.camera.worldToLocal(this.plane.position) 128 | 129 | } 130 | 131 | addEventListeners() { 132 | this.renderer.domElement.addEventListener('pointermove', (event)=>{ 133 | this.updateCursorPosition(event); 134 | } 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /MonkeyPaint.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import * as THREE from "three"; 4 | import { OrbitControls } from "three/addons/controls/OrbitControls.js"; 5 | import { RGBELoader } from "three/addons/loaders/RGBELoader.js"; 6 | import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; 7 | import { GLTFExporter } from "three/addons/exporters/GLTFExporter.js"; 8 | import { GUI } from "three/addons/libs/lil-gui.module.min.js"; 9 | import CursorNode from "./CursorNode.js" 10 | 11 | const { Scene, WebGLRenderer, PerspectiveCamera, Mesh, BufferGeometry, CircleGeometry, BoxGeometry, MeshBasicMaterial, Vector3, AnimationMixer, Object3D, TextureLoader, Sprite, SpriteMaterial, RepeatWrapping, } = THREE; 12 | const vec3 = Vector3; 13 | const { random, abs, sin, cos, min, max, floor } = Math; 14 | const rnd = (rng = 1) => random() * rng; 15 | const srnd = (rng = 1) => random() * rng * 2 - rng; 16 | 17 | THREE.Cache.enabled = true; 18 | 19 | const v0 = new vec3(); 20 | const v1 = new vec3(); 21 | const v2 = new vec3(); 22 | const v3 = new vec3(); 23 | 24 | console.log("Hello 🌎 [ᚠ] ᚢ ᚦ ᚨ ᚱ ᚲ ᚷ [ᚹ] ᚺ ᚾ ᛁ ᛃ ᛈ ᛇ ᛉ ᛊ ᛏ ᛒ ᛖ ᛗ ᛚ ᛜ ᛞ ᛟ"); 25 | 26 | const renderer = new WebGLRenderer({ 27 | //antialias: false, 28 | alpha: true, 29 | //logarithmicDepthBuffer: true 30 | }); 31 | renderer.outputColorSpace = 'srgb-linear'; 32 | 33 | renderer.setClearColor(0xcccccc); 34 | 35 | const stl = renderer.domElement.style; 36 | stl.position = "absolute"; 37 | stl.left = stl.top = "0px"; 38 | 39 | document.body.appendChild(renderer.domElement); 40 | 41 | const scene = new Scene(); 42 | const camera = new PerspectiveCamera(); 43 | scene.add(camera); 44 | const controls = new OrbitControls(camera, renderer.domElement); 45 | camera.position.set(-0.8, 0.5, 1.8); 46 | controls.target.set(0, 0.5, 0.0); 47 | controls.maxPolarAngle = Math.PI; 48 | controls.minPolarAngle = -Math.PI; 49 | 50 | renderer.shadowMap.enabled = true; 51 | renderer.shadowMap.type = THREE.PCFSoftShadowMap; 52 | // default THREE.PCFShadowMap 53 | 54 | const lightRig = new THREE.Object3D(); 55 | 56 | //Create a DirectionalLight and turn on shadows for the light 57 | const dlight = new THREE.DirectionalLight(0xffffff, 0.1); 58 | dlight.position.set(0, 50, 0); 59 | dlight.castShadow = true; 60 | 61 | //Set up shadow properties for the light 62 | dlight.shadow.mapSize.width = 1024; 63 | dlight.shadow.mapSize.height = 1024; 64 | dlight.shadow.camera.near = 0.5; 65 | dlight.shadow.camera.far = 100; 66 | dlight.shadow.camera.left = dlight.shadow.camera.bottom = -18; 67 | dlight.shadow.camera.top = dlight.shadow.camera.right = 18; 68 | 69 | //dlight.shadow.bias = -0.01; 70 | //scene.fog = new THREE.Fog(0xcccccc, 48, 50); 71 | 72 | const lightParam = { 73 | pitch: -0.13, 74 | yaw: -2.2, 75 | }; 76 | camera.position.set(.5, .5, 2.7); 77 | controls.target.set(0, 0, 0); 78 | const ambientLight = new THREE.AmbientLight("white", 0.1) 79 | dlight.intensity = 0.8; 80 | 81 | dlight.target.position.set(0, 0.5, 0); 82 | 83 | lightRig.add(ambientLight); 84 | lightRig.add(dlight); 85 | lightRig.add(dlight.target); 86 | 87 | //let pointLight = new THREE.PointLight(); 88 | //pointLight.position.y+=.5; 89 | //lightRig.add(pointLight) 90 | 91 | scene.add(lightRig); 92 | 93 | const repoLight = () => { 94 | const { pitch, yaw } = lightParam; 95 | const lpitch = Math.sin(pitch); 96 | dlight.position.set(Math.sin(yaw) * lpitch, Math.cos(pitch), Math.cos(yaw) * lpitch); 97 | dlight.position.multiplyScalar(1.5); 98 | dlight.lookAt(dlight.target.position); 99 | dlight.updateMatrix(); 100 | dlight.updateMatrixWorld(); 101 | }; 102 | repoLight(); 103 | 104 | const gui = new GUI({ 105 | width: 200, 106 | visible: false, 107 | }); 108 | 109 | gui.add(lightParam, "pitch", -Math.PI * 1., 0).name("LightPitch").onChange((val) => { 110 | repoLight(); 111 | }); 112 | gui.add(lightParam, "yaw", -Math.PI, Math.PI).name("LightYaw").onChange((val) => { 113 | repoLight(); 114 | }); 115 | 116 | gui.add({ 117 | wireframe: false 118 | }, "wireframe").name("Wireframe").onChange((val) => { 119 | scene.traverse(e => e.isMesh && (e.material.wireframe = val ? true : false)) 120 | }); 121 | 122 | const pmremGenerator = new THREE.PMREMGenerator(renderer); 123 | pmremGenerator.compileEquirectangularShader(); 124 | let envMap; 125 | new RGBELoader().setPath("").load("https://cdn.glitch.global/364206c7-9713-48db-9215-72a591a6a9bd/pretville_street_1k.hdr?v=1658931258610", function (texture) { 126 | envMap = pmremGenerator.fromEquirectangular(texture).texture; 127 | 128 | //scene.background = envMap; 129 | scene.environment = envMap; 130 | 131 | texture.dispose(); 132 | pmremGenerator.dispose(); 133 | }); 134 | 135 | controls.enableDamping = true; 136 | 137 | const onWindowResize = (event) => { 138 | const width = window.innerWidth; 139 | const height = window.innerHeight; 140 | renderer.setSize(width, height); 141 | camera.aspect = width / height; 142 | camera.updateProjectionMatrix(); 143 | }; 144 | 145 | onWindowResize(); 146 | window.addEventListener("resize", onWindowResize, false); 147 | 148 | const cursorNode = new CursorNode(camera, renderer); 149 | renderer.domElement.style.cursor = 'crosshair' 150 | 151 | import PainterApp from "./ScenePainter.js" 152 | 153 | const painterApp = new PainterApp({ THREE, GLTFLoader, GLTFExporter, scene, camera, controls, renderer, gui, cursorNode }) 154 | 155 | renderer.render(scene, camera) 156 | 157 | //new GLTFLoader().load("teeth.glb", (glb)=>{ 158 | //new GLTFLoader().load("male-form.glb", (glb)=>{ 159 | //new GLTFLoader().load("female-form.glb", (glb)=>{ 160 | const glb = await new GLTFLoader().loadAsync("monkeh.glb"); 161 | const scenePainter = new painterApp.ScenePainter(glb.scene); 162 | 163 | //new GLTFLoader().load("den.gltf", (glb)=>{ 164 | //new GLTFLoader().load("CartoonTV_bake.glb", (glb)=>{ 165 | // let scenePainter = new painterApp.ScenePainter(glb.scene); 166 | //} 167 | //); 168 | 169 | renderer.setAnimationLoop((time) => { 170 | scenePainter.update(time); 171 | painterApp.render(time) 172 | }) 173 | 174 | export { THREE, GLTFLoader, GLTFExporter, gui, scene, camera, controls, renderer, cursorNode } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # monkeypaint 2 | An app for painting on meshes in THREEJS, with glb export. 3 | 4 | Try it here!: https://manthrax.github.io/monkeypaint/index.html?1 5 | 6 | ![image](https://github.com/manthrax/monkeypaint/assets/350247/864c5544-8145-4dd4-9d0e-6c0038196ede) 7 | 8 | This implements 3d mesh painting using via the gpu and rendertargets. 9 | 10 | Tested with threejs r160.. 11 | For some older versions replace all "vMapUv" in ScenePainter.js 12 | with "vUv" 13 | 14 | Algorithm: 15 | First, the UV map is rendered as a 3d model onto a renderTarget in white, to form a binary mask of which pixels are covered by the UVmap 16 | 17 | Then the UV map is rendered a second time, with the models texture bound. The 3d vertex coordinates are also available since 18 | the UV rendering behavior is injected into the material using shader injection. 19 | 20 | As each UV triangle is rendered.. the vertex coordinate is transformed to world space and compared with the Brush position in worldspace 21 | (derived from the cursor raycast) 22 | This computes an intensity 0 to 1 for the brush affecting this texel of the UV map. 23 | Brush color is mixed with existing texel color and output. 24 | 25 | Next, a "dilation" shader is run with the UV mask texture bound. For every texel not underneath a UV triangle (i.e. edges of UV islands) 26 | The nearest texel that is in a UV island (within 16 pixels or so) is found , and that pixel color is output. 27 | This step eliminates most of the seams that occur due to the filtering of textures, 28 | by giving the islands a 16 or so pixel padding for the filtering to access on island boundaries. 29 | 30 | Export: 31 | intermediate rendertargets containing the painted texture, are converted to canvas texture, then run through 32 | the binary GLTF exporter as a binary gltf file (.glb) 33 | 34 | 35 | Save/Load/Reset: 36 | these are a bit janky.. When you hit Save, all the brush strokes in the seesion are saved to localstorage. 37 | so if you reload the page, and then hit Load, it will repaint the model as you had it before. 38 | Reset clears the current localStorage save and reloads the page. 39 | 40 | TODO: 41 | implement custom brush shapes. right now it's just a variable sized sphere.. and doesn't orient along the ray hit normal. 42 | Allow texture stamping as an extension of this. 43 | 44 | Allow specifying different texture outputs for roughnessmap, metalnessmap, opacitymap, and normalmap. 45 | -------------------------------------------------------------------------------- /ScenePainter.js: -------------------------------------------------------------------------------- 1 | /*********** 2 | 3 | MESH TEXTURE PAINTER by Thrax (C) - manthrax@gmail && vectorslave.com 4 | 5 | ***********/ 6 | 7 | import UnwrapUVs from "./UnwrapUVs.js" 8 | 9 | import { GLTFExporter } from "three/addons/exporters/GLTFExporter.js"; 10 | export default function PaintApp({ THREE, GLTFLoader, scene, camera, controls, renderer, gui, cursorNode, uPaintSourceTexture }) { 11 | 12 | let patch = (obj, fld, ck, fn) => obj[fld] = obj[fld].replace(`#include <${ck}>`, fn(THREE.ShaderChunk[ck], `#include <${ck}>`)); 13 | 14 | function MeshPainter(sourceMesh) { } 15 | 16 | 17 | 18 | 19 | 20 | function ScenePainter(paintScene) { 21 | let brushUniforms = { 22 | uBrushStrength: { 23 | value: 0.2 24 | }, 25 | uBrushHardness: { 26 | value: 0.2 27 | }, 28 | uBrushSize: { 29 | value: new THREE.Vector3(.1, .1, .1) 30 | }, 31 | uBrushColor: { 32 | value: new THREE.Vector4(1, 0, 0, 1) 33 | } 34 | } 35 | scene.add(paintScene); 36 | 37 | let paintMaterials = [] 38 | let paintMeshes = [] 39 | paintScene.traverse(e => e.isMesh && e.material && (paintMaterials.push(e.material) | paintMeshes.push(e))) 40 | 41 | paintMeshes.forEach(e => e.visible = false); 42 | 43 | let query = window.location.search.substring(1).split("="); 44 | let id = parseInt(query); 45 | if (!((id >= 0) && (id < paintMeshes.length))) 46 | id = 2; 47 | let sourceMesh = paintMeshes[id % paintMeshes.length] 48 | 49 | sourceMesh.visible = true; 50 | 51 | let bounds = new THREE.Box3().setFromObject(sourceMesh); 52 | let size = bounds.getSize(new THREE.Vector3()); 53 | let { max } = Math; 54 | let maxsz = max(size.x, max(size.y, size.z)); 55 | //Rescale the model.. 56 | sourceMesh.scale.multiplyScalar(1 / maxsz); 57 | sourceMesh.updateMatrixWorld() 58 | bounds.setFromObject(sourceMesh); 59 | sourceMesh.position.sub(bounds.getCenter(new THREE.Vector3())) 60 | 61 | /* 62 | //Recenter camera/controls on model 63 | bounds.getCenter(controls.target) 64 | camera.position.copy(controls.target) 65 | camera.position.z += maxsz * 1.5; 66 | */ 67 | 68 | if (!sourceMesh.material.map) { 69 | alert("Object has no Texture/Material assigned.. ") 70 | let canv = document.createElement('canvas'); 71 | canv.width = canv.height = 1024; 72 | let ctx = canv.getContext('2d'); 73 | ctx.fillStyle = 'white' 74 | ctx.fillRect(0, 0, canv.width, canv.height) 75 | //sourceMesh.material.map = new THREE.CanvasTexture(canv) 76 | sourceMesh.material.map = new THREE.Texture(canv) 77 | sourceMesh.material.map.colorSpace = THREE.SRGBColorSpace; 78 | sourceMesh.material.map.needsUpdate = true; 79 | } 80 | 81 | if (!sourceMesh.geometry.attributes.uv) { 82 | alert("Object has no UV map.. ") 83 | UnwrapUVs(sourceMesh.geometry); 84 | } 85 | 86 | /* 87 | let p=sourceMesh.position.clone().sub(controls.target); 88 | controls.target.add(p); 89 | camera.position.add(p); 90 | */ 91 | // sourceMesh.position.x -= .4; 92 | 93 | let texTransformer = new TextureTransformer(sourceMesh.material.map, renderer) 94 | 95 | let previewPlane = new THREE.Mesh(new THREE.PlaneGeometry(), new THREE.MeshBasicMaterial({ 96 | map: texTransformer.feedbackTexture.renderTarget.texture, 97 | depthWrite: false 98 | })); 99 | camera.add(previewPlane); 100 | previewPlane.rotation.set(0, 0, 0); 101 | previewPlane.position.set(-.1, 0, -1.2); 102 | previewPlane.scale.set(1, 1, 1); 103 | previewPlane.visible = false; 104 | previewPlane.material.onBeforeCompile = (shader) => { 105 | shader.fragmentShader = shader.fragmentShader.replace('}', ` 106 | if(sampledDiffuseColor.a<.1) 107 | sampledDiffuseColor.rgb=vec3(0,0,1); 108 | }`) 109 | } 110 | 111 | let paintMesh = sourceMesh.clone(); 112 | paintMesh.material = paintMesh.material.clone(); 113 | 114 | let setEnvBrightness = (v) => { 115 | scene.traverse(e => e.isMesh && e.material && (e.material.envMapIntensity = v)); 116 | } 117 | 118 | this.exportScene = () => { 119 | 120 | const exporter = new GLTFExporter(); 121 | //const scene = new THREE.Scene(); 122 | //const mesh = new THREE.Mesh(new THREE.BoxGeometry(1,1,1),new THREE.MeshBasicMaterial({ 123 | // color: 0xff0000 124 | //})); 125 | //scene.add(mesh); 126 | 127 | const link = document.createElement('a'); 128 | link.style.display = 'none'; 129 | document.body.appendChild(link); 130 | // Firefox workaround, see #6594 131 | 132 | function save(blob, filename) { 133 | link.href = URL.createObjectURL(blob); 134 | link.download = filename; 135 | link.click(); 136 | // URL.revokeObjectURL( url ); breaks Firefox... 137 | } 138 | function saveArrayBuffer(buffer, filename) { 139 | save(new Blob([buffer], { 140 | type: 'application/octet-stream' 141 | }), filename); 142 | } 143 | const options = { 144 | trs: true, 145 | onlyVisible: true, 146 | binary: true, 147 | maxTextureSize: 4096 //params.maxTextureSize 148 | }; 149 | 150 | scene.traverse(e => { 151 | if (e.isMesh && e.material.map && e.material.map.userData.renderTargetId) { 152 | let rt = FeedbackTexture.renderTargetMap[e.material.map.userData.renderTargetId]; 153 | 154 | // rt = texTransformer.feedbackTexture.renderTarget; 155 | 156 | let canvas = renderTargetToCanvas(renderer, rt); 157 | e.material.map = new THREE.CanvasTexture(canvas); 158 | //null; 159 | e.material.map.flipY = false; 160 | } 161 | } 162 | ) 163 | exporter.parse(scene, (result) => { 164 | if (result instanceof ArrayBuffer) { 165 | saveArrayBuffer(result, 'scene.glb'); 166 | } 167 | } 168 | , function (error) { 169 | console.log('An error happened during parsing', error); 170 | }, options); 171 | } 172 | 173 | this.update = (time) => { 174 | 175 | //let time = performance.now(); 176 | // if(!lastTime)lastTime=time; 177 | // fdt=time-lastTime; 178 | // if(fdt= maxSubsteps) { 190 | simTime = time; 191 | break; 192 | } 193 | } 194 | if (exportTriggered) { 195 | exportTriggered = false; 196 | this.exportScene(); 197 | } 198 | } 199 | gui.add({ 200 | export: () => { 201 | exportTriggered = true; 202 | } 203 | }, "export").name("export glb!"); 204 | 205 | (paintMesh.material.roughness !== undefined) && gui.add(paintMesh.material, "roughness", 0, 1); 206 | (paintMesh.material.metalness !== undefined) && gui.add(paintMesh.material, "metalness", 0, 1); 207 | 208 | gui.add({ 209 | intensity: .25 210 | }, "intensity", 0, 1).name("env brightness:").onChange(setEnvBrightness) 211 | gui.add(previewPlane, 'visible').name('tex : ' + paintMesh.name); 212 | 213 | gui.add(brushUniforms.uBrushStrength, "value", 0, 1.).name("Strength") 214 | gui.add(brushUniforms.uBrushHardness, "value", 0, 1.).name("Hardness") 215 | gui.add({ 216 | value: .01 217 | }, "value", .002, .2, 0.0001).name("Size").onChange(v => { 218 | brushUniforms.uBrushSize.value.x = v;//(Math.pow(v, 3.) * 3.) + .01; 219 | } 220 | ) 221 | 222 | const colorFormats = { 223 | string: '#ff0000', 224 | int: 0xff0000, 225 | object: { 226 | r: 1, 227 | g: 0, 228 | b: 0 229 | }, 230 | array: [1, 0, 0] 231 | }; 232 | let tc = new THREE.Color(); 233 | gui.addColor(colorFormats, 'string').onChange(v => { 234 | tc.set(v); 235 | brushUniforms.uBrushColor.value.set(tc.r, tc.g, tc.b, 1); 236 | } 237 | ) 238 | 239 | let uCamViewMatrix = { 240 | value: camera.matrixWorldInverse 241 | } 242 | let uCamProjectionMatrix = { 243 | value: camera.projectionMatrix 244 | } 245 | let uBrushPoint = { 246 | value: new THREE.Vector3() 247 | } 248 | let uBrushNormal = { 249 | value: new THREE.Vector3() 250 | } 251 | let uCursorPosition = { 252 | value: cursorNode.fragMouse 253 | } 254 | let uMatrixWorld = { 255 | value: paintMesh.matrixWorld 256 | } 257 | let setUniforms = (shader) => { 258 | shader.uniforms.uMatrixWorld = uMatrixWorld 259 | shader.uniforms.uCamViewMatrix = uCamViewMatrix 260 | shader.uniforms.uCamProjectionMatrix = uCamProjectionMatrix 261 | shader.uniforms.uBrushNormal = uBrushNormal; 262 | shader.uniforms.uBrushPoint = uBrushPoint; 263 | shader.uniforms.uCursorPosition = uCursorPosition; 264 | shader.uniforms.uPaintSourceTexture = uPaintSourceTexture; 265 | 266 | Object.assign(shader.uniforms, brushUniforms) 267 | } 268 | 269 | let computeBrushInfluence = ` 270 | float brushInfluence = 0.; 271 | vec3 brushDelta = (vWorldPosition-uBrushPoint) / uBrushSize.x; 272 | 273 | vec4 brushScreen = uCamViewMatrix * vec4(vWorldPosition,1.); 274 | brushScreen = uCamProjectionMatrix * brushScreen; 275 | brushScreen /= brushScreen.w; 276 | 277 | float dist = min(1.,length(brushDelta)); 278 | dist = length(brushDelta); 279 | brushInfluence = max(0.,1.-pow(dist,.1+(uBrushHardness*15.))); 280 | brushInfluence *= uBrushStrength; 281 | ` 282 | let brushVars = ` 283 | 284 | uniform vec4 uBrushColor; 285 | uniform vec3 uBrushPoint; 286 | uniform vec3 uBrushSize; 287 | uniform float uBrushHardness; 288 | uniform float uBrushStrength; 289 | uniform vec2 uCursorPosition; 290 | varying vec3 vWorldPosition; 291 | 292 | uniform mat4 uCamViewMatrix; 293 | uniform mat4 uCamProjectionMatrix; 294 | 295 | uniform sampler2D uPaintSourceTexture; 296 | ` 297 | 298 | //This shader renders a model with the ghost of the brush cursor rendered on top... 299 | paintMesh.material.onBeforeCompile = (shader, renderer) => { 300 | shader.vertexShader = ` 301 | uniform mat4 uMatrixWorld; 302 | varying vec3 vWorldPosition; 303 | ` + shader.vertexShader; 304 | 305 | patch(shader, 'vertexShader', `fog_vertex`, (ck, ckinc) => { 306 | return ckinc + ` 307 | vWorldPosition = (uMatrixWorld * vec4(transformed,1.0)).xyz; 308 | 309 | ` 310 | } 311 | ) 312 | shader.fragmentShader = ` 313 | ${brushVars} 314 | 315 | ` + shader.fragmentShader 316 | 317 | shader.fragmentShader = shader.fragmentShader.replace('}', ` 318 | #ifdef USE_MAP 319 | //gl_FragColor=vec4(.0,fract(vMapUv*10.)*.2,1.); 320 | #endif 321 | 322 | ${computeBrushInfluence} 323 | 324 | brushInfluence = max(0., brushInfluence - smoothstep(.1,0.,brushInfluence)); 325 | gl_FragColor = mix(gl_FragColor,vec4(1.)-gl_FragColor,brushInfluence); 326 | 327 | //brushInfluence = max(0., brushInfluence - smoothstep(brushInfluence,.1,0.)); 328 | //gl_FragColor = mix(gl_FragColor,uBrushColor,brushInfluence); 329 | 330 | } 331 | `) 332 | setUniforms(shader) 333 | } 334 | 335 | let drawing = false; 336 | let buttons = 0; 337 | document.addEventListener('pointerdown', (e) => { 338 | if (e.target !== renderer.domElement) return; 339 | buttons = e.buttons; 340 | if (buttons == 1) { 341 | (controls.enabled = !(cursorNode.raycast(paintMesh).length > 0)); 342 | if (!controls.enabled) 343 | drawing = true; 344 | } 345 | } 346 | ); 347 | document.addEventListener('pointerup', (e) => { 348 | controls.enabled = true; 349 | drawing = false; 350 | buttons = e.buttons; 351 | } 352 | ) 353 | 354 | let uvMesh = sourceMesh; 355 | uvMesh.parent.add(paintMesh); 356 | uvMesh.material = uvMesh.material.clone(); 357 | 358 | camera.add(uvMesh); 359 | uvMesh.rotation.set(0, 0, 0); 360 | uvMesh.position.set(-4.5, -3, -20); 361 | uvMesh.scale.set(2, 2, 2); 362 | uvMesh.position.set(-1, -1, 0); 363 | texTransformer.uvScene.add(uvMesh); 364 | 365 | let dilator = new Dilator(texTransformer, uvMesh, previewPlane, paintMesh); 366 | 367 | renderer.setClearColor(0x101010); 368 | 369 | let replay = [] 370 | let replaying = false; 371 | let replayCursor = 0; 372 | let repeatCountdown = 0; 373 | 374 | let load = () => { 375 | try { 376 | if (localStorage.monkeyReplay) { 377 | replay = JSON.parse(localStorage.monkeyReplay); 378 | if (replay.length) 379 | replaying = true; 380 | } 381 | } catch { 382 | alert("Couldn't parse localstorage! Replay lost...") 383 | replay = [] 384 | } 385 | } 386 | let getState = () => { 387 | return { 388 | uBrushPoint: uBrushPoint.value.clone(), 389 | uBrushNormal: uBrushNormal.value.clone(), 390 | uBrushSize: brushUniforms.uBrushSize.value.clone(), 391 | uBrushColor: brushUniforms.uBrushColor.value.clone(), 392 | uBrushHardness: brushUniforms.uBrushHardness.value, 393 | uBrushStrength: brushUniforms.uBrushStrength.value, 394 | _repeat: 1, 395 | } 396 | } 397 | let setState = (st) => { 398 | uBrushPoint.value.copy(st.uBrushPoint) 399 | uBrushNormal.value.copy(st.uBrushNormal) 400 | brushUniforms.uBrushSize.value.copy(st.uBrushSize) 401 | brushUniforms.uBrushColor.value.copy(st.uBrushColor) 402 | brushUniforms.uBrushHardness.value = st.uBrushHardness 403 | brushUniforms.uBrushStrength.value = st.uBrushStrength 404 | // console.log(st.uBrushPoint) 405 | } 406 | let statesEqual = (sa, sb) => { 407 | if (typeof sa == 'object') { 408 | for (let f in sa) 409 | if ((!f.startsWith('_')) && (!statesEqual(sa[f], sb[f]))) 410 | return false; 411 | } else 412 | return (sa == sb); 413 | return true; 414 | } 415 | 416 | let lastState; 417 | // window.onBeforeUnload=()=>{ 418 | // if(replay.length){ 419 | // localStorage.monkeyReplay = JSON.stringify(replay); 420 | // } 421 | // } 422 | 423 | gui.add({ 424 | save: () => { 425 | localStorage.monkeyReplay = JSON.stringify(replay); 426 | console.log("Save size:", localStorage.monkeyReplay.length) 427 | } 428 | }, "save") 429 | gui.add({ 430 | load 431 | }, "load") 432 | 433 | gui.add({ 434 | reset: () => { 435 | replay = [] 436 | //delete localStorage.monkeyReplay; 437 | location.reload(); 438 | } 439 | }, "reset") 440 | let updateReplay = () => { 441 | if (replaying) { 442 | if (replayCursor >= replay.length) { 443 | replaying = false; 444 | lastState = undefined; 445 | } else { 446 | let state = replay[replayCursor]; 447 | if (!repeatCountdown) 448 | repeatCountdown = state._repeat; 449 | else { 450 | repeatCountdown--; 451 | if (!repeatCountdown) 452 | replayCursor++; 453 | } 454 | setState(state); 455 | } 456 | } else { 457 | let state = getState(); 458 | if (lastState) { 459 | if (statesEqual(state, lastState)) 460 | lastState._repeat++; 461 | else 462 | replay.push(lastState) 463 | } 464 | lastState = state; 465 | } 466 | } 467 | let draw = () => { 468 | let fbt = texTransformer.feedbackTexture; 469 | let iterations = 0; 470 | do { 471 | try { 472 | updateReplay(); 473 | } catch (e) { 474 | console.log(e) 475 | replaying = false; 476 | replay = [] 477 | } 478 | uvMesh.material.map = fbt.renderTarget.texture; 479 | texTransformer.renderUVMeshToTarget(fbt.offRenderTarget); 480 | previewPlane.material.map = fbt.renderTarget.texture; 481 | paintMesh.material.map = fbt.renderTarget.texture 482 | uvMesh.material.map = fbt.offRenderTarget.texture; 483 | 484 | iterations++; 485 | } while (replaying && (iterations < 100)) dilator.apply(fbt); 486 | } 487 | 488 | //Convert rendertarget to canvasTexture 489 | function renderTargetToCanvas(renderer, renderTarget, canvas = document.createElement('canvas')) { 490 | const width = renderTarget.width; 491 | const height = renderTarget.height; 492 | canvas.width = width; 493 | canvas.height = height; 494 | const ctx = canvas.getContext('2d'); 495 | const imageData = ctx.createImageData(width, height); 496 | if (renderTarget.texture.type == THREE.FloatType) { 497 | const buffer = new Float32Array(width * height * 4); 498 | renderer.readRenderTargetPixels(renderTarget, 0, 0, width, height, buffer); 499 | for (let i = 0; i < buffer.length; i++) 500 | buffer[i] *= 255; 501 | imageData.data.set(buffer); 502 | } else { 503 | const buffer = new Uint8Array(width * height * 4); 504 | renderer.readRenderTargetPixels(renderTarget, 0, 0, width, height, buffer); 505 | imageData.data.set(buffer); 506 | } 507 | ctx.putImageData(imageData, 0, 0); 508 | return canvas; 509 | } 510 | 511 | paintMesh.onBeforeRender = () => { 512 | let hits = cursorNode.raycast(paintMesh) 513 | if (hits[0]) { 514 | //controls.enabled = false; 515 | let v = uBrushPoint.value; 516 | v.copy(hits[0].point) 517 | //paintMesh.worldToLocal(v); 518 | let n = uBrushNormal.value; 519 | n.copy(hits[0].face.normal).add(v) 520 | //paintMesh.worldToLocal(n); 521 | n.sub(v); 522 | 523 | } else { 524 | let v = uBrushPoint.value; 525 | uBrushPoint.value.set(0, -1000, 0) 526 | } 527 | if ((buttons == 1) && (drawing || replaying)) 528 | draw(); 529 | } 530 | 531 | //This shader renders a models UV coordinates as polygons, and applies the influence of the brush... rendering the current brush stroke when mouse is down... 532 | uvMesh.material.onBeforeCompile = (shader, renderer) => { 533 | if (shader.vertexShader.indexOf('normal_pars_vertex') < 0) { 534 | shader.vertexShader = shader.vertexShader.replace(`#include `, ` 535 | #include 536 | varying vec3 vNormal; 537 | `) 538 | shader.vertexShader = shader.vertexShader.replace(`#include `, ` 539 | #include 540 | #include 541 | #include 542 | vNormal = normalize( transformedNormal ); 543 | `) 544 | } 545 | 546 | shader.vertexShader = ` 547 | varying vec3 vWorldPosition; 548 | uniform mat4 uMatrixWorld; 549 | ` + shader.vertexShader; 550 | 551 | patch(shader, 'vertexShader', 'begin_vertex', (ck) => { 552 | return ` 553 | #include 554 | #ifdef USE_MAP 555 | vWorldPosition = (uMatrixWorld * vec4(transformed,1.)).xyz; 556 | transformed = vec3(vMapUv,0.); 557 | #endif 558 | ` 559 | } 560 | ) 561 | 562 | shader.fragmentShader = ` 563 | 564 | ${brushVars} 565 | 566 | ` + shader.fragmentShader; 567 | shader.fragmentShader = shader.fragmentShader.replace('}', ` 568 | #ifdef USE_MAP 569 | //gl_FragColor=vec4(fract(vMapUv*10.),0.,1.); 570 | 571 | ${computeBrushInfluence} 572 | 573 | //BAKE 574 | //gl_FragColor.rgb = mix(gl_FragColor,uBrushColor,brushInfluence); 575 | 576 | 577 | //PAINT 578 | 579 | vec4 brushColor = uBrushColor; 580 | 581 | ${uPaintSourceTexture ? ` 582 | 583 | //Transfer brush from paintSourceTexture 584 | vec2 brushUV = (brushScreen.xy*.5)+.5; 585 | brushColor = texture2D(uPaintSourceTexture, brushUV); 586 | 587 | 588 | ` : ``} 589 | 590 | gl_FragColor = mix(sampledDiffuseColor,brushColor,brushInfluence); 591 | return; 592 | 593 | /* 594 | // BACK FACE CULLING... 595 | vec3 camVert = vWorldPosition-uCamViewMatrix[3].xyz; 596 | vec3 viewNormal = mat3(uCamViewMatrix) * vNormal; 597 | if(dot(normalize(viewNormal),camVert)<0.) 598 | gl_FragColor = sampledDiffuseColor; 599 | // BACK FACE CULLING... 600 | */ 601 | 602 | 603 | #endif 604 | } 605 | `) 606 | setUniforms(shader); 607 | 608 | } 609 | 610 | setEnvBrightness(.25); 611 | } 612 | 613 | function FeedbackTexture(texture, renderer) { 614 | 615 | let makeTarget = this.makeTarget = () => { 616 | let rt = new THREE.WebGLRenderTarget(texture.image.width, texture.image.height, { 617 | format: THREE.RGBAFormat, 618 | type: THREE.UnsignedByteType, 619 | //THREE.FloatType, 620 | //minFilter: THREE.NearestFilter, 621 | //magFilter: THREE.NearestFilter, 622 | depthBuffer: false, 623 | stencilBuffer: false, 624 | //encoding: THREE.LinearEncoding 625 | colorSpace: THREE.NoColorSpace //""//THREE.LinearSRGBColorSpace 626 | }); 627 | if (!FeedbackTexture.renderTargetMap) { 628 | FeedbackTexture.renderTargetMap = {} 629 | } 630 | 631 | rt.texture.userData.renderTargetId = rt.texture.uuid; 632 | FeedbackTexture.renderTargetMap[rt.texture.uuid] = rt; 633 | return rt; 634 | } 635 | 636 | let renderTarget = this.renderTarget = makeTarget() 637 | let offRenderTarget = this.offRenderTarget = makeTarget() 638 | 639 | // Create a scene with a mesh that uses the texture 640 | const geometry = new THREE.PlaneGeometry(2, 2); 641 | const material = this.material = new THREE.MeshBasicMaterial({ 642 | map: texture 643 | }); 644 | 645 | const mesh = this.mesh = new THREE.Mesh(geometry, material); 646 | const scene = new THREE.Scene(); 647 | scene.add(mesh); 648 | 649 | const camera = this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); 650 | // Render the scene to the render target 651 | let saveTarget = renderer.getRenderTarget(); 652 | renderer.setRenderTarget(renderTarget); 653 | renderer.render(scene, camera); 654 | renderer.setRenderTarget(saveTarget); 655 | 656 | let swap = this.swap = () => { 657 | let sv = renderTarget; 658 | this.renderTarget = renderTarget = offRenderTarget; 659 | this.offRenderTarget = offRenderTarget = sv; 660 | } 661 | this.renderOperation = (destination, fn) => { 662 | let saveTarg = renderer.getRenderTarget(); 663 | renderer.setRenderTarget(destination); 664 | 665 | let acSave = renderer.autoClearColor; 666 | renderer.autoClearColor = false; 667 | 668 | fn(renderer, scene, camera); 669 | 670 | renderer.autoClearColor = acSave; 671 | 672 | renderer.setRenderTarget(saveTarg); 673 | } 674 | } 675 | 676 | function TextureTransformer(texture, renderer) { 677 | // Create a texture to render into the render target 678 | 679 | // Set up the render target 680 | this.feedbackTexture = new FeedbackTexture(texture, renderer); 681 | 682 | const uvScene = this.uvScene = new THREE.Scene(); 683 | 684 | this.renderUVMeshToTarget = (destination) => { 685 | 686 | this.feedbackTexture.renderOperation(destination, (renderer, scene, camera) => { 687 | renderer.render(scene, camera); 688 | renderer.render(uvScene, camera); 689 | } 690 | ); 691 | this.feedbackTexture.swap() 692 | 693 | } 694 | } 695 | 696 | function Dilator(texTransformer, uvMesh, previewPlane, paintMesh) { 697 | let sourceTexture = uvMesh.material.map; 698 | 699 | this.feedbackTexture = new FeedbackTexture(sourceTexture, renderer); 700 | 701 | let uvMask = new UVMask(texTransformer, uvMesh.material, uvMesh); 702 | 703 | previewPlane.material.map = uvMask.uvMaskTarget.texture; 704 | 705 | let dilationMapShader = uvMesh.material.clone(); 706 | let uPaintTextureSize = { 707 | value: new THREE.Vector2(sourceTexture.source.data.width, sourceTexture.source.data.height) 708 | } 709 | let uPaintTexture = { 710 | value: paintMesh.material.map 711 | } 712 | dilationMapShader.onBeforeCompile = (shader, renderer) => { 713 | shader.fragmentShader = ` 714 | uniform sampler2D uPaintTexture; 715 | uniform vec2 uPaintTextureSize; 716 | ` + shader.fragmentShader 717 | shader.uniforms.uPaintTextureSize = uPaintTextureSize; 718 | shader.uniforms.uPaintTexture = uPaintTexture; 719 | //This shader hack dilates the UV island textures outwards to remove seams. 720 | shader.fragmentShader = shader.fragmentShader.replace('}', ` 721 | #ifdef USE_MAP 722 | vec4 maskColor = texture2D( map, vMapUv); //map is the mask texture 723 | if(maskColor.r<.99){ 724 | //OUtside the mask 725 | gl_FragColor.rgba = vec4(0.); 726 | const float rad=16.; 727 | float closestDistance = sqrt(rad*rad); 728 | vec4 closestColor = vec4(0.); 729 | for( float y=-rad;y .9){ 735 | float distance = length(vec2(x,y)); 736 | if(distance { 754 | 755 | this.feedbackTexture.mesh.material = dilationMapShader 756 | 757 | dilationMapShader.map = uvMask.uvMaskTarget.texture; 758 | 759 | uPaintTexture.value = sourceFBT.renderTarget.texture; 760 | 761 | this.feedbackTexture.renderOperation(this.feedbackTexture.offRenderTarget, (renderer, scene, camera) => { 762 | renderer.render(scene, camera); 763 | } 764 | ); 765 | this.feedbackTexture.swap(); 766 | paintMesh.material.map = previewPlane.material.map = this.feedbackTexture.renderTarget.texture; 767 | 768 | } 769 | } 770 | 771 | function UVMask(transformer, material, uvMesh) { 772 | let uvMaskShader = material.clone(); 773 | uvMaskShader.side = THREE.DoubleSide;//This sumbitch took me for a ride... 774 | uvMaskShader.onBeforeCompile = (shader, renderer) => { 775 | //This shader renders out a mask channel with vec4(1.) where the texel is covered by the UV and 0 otherwise. 776 | 777 | // shader.vertexShader = `#extension GL_ANGLE_multi_draw : require 778 | // `+shader.vertexShader; 779 | 780 | shader.vertexShader = ` 781 | varying vec3 vWorldPosition; 782 | uniform mat4 uMatrixWorld; 783 | ` + shader.vertexShader; 784 | 785 | patch(shader, 'vertexShader', 'begin_vertex', (ck, ckinc) => ckinc + ` 786 | #ifdef USE_MAP 787 | vWorldPosition = (uMatrixWorld * vec4(transformed,1.)).xyz; 788 | transformed = vec3(vMapUv,0.); 789 | #endif 790 | `) 791 | shader.fragmentShader = shader.fragmentShader.replace('}', ` 792 | gl_FragColor.rgb=vec3(1.); 793 | }`) 794 | } 795 | 796 | this.uvMaskTarget = transformer.feedbackTexture.makeTarget(); 797 | 798 | let svMat = uvMesh.material; 799 | uvMesh.material = uvMaskShader; 800 | 801 | renderer.setRenderTarget(this.uvMaskTarget); 802 | 803 | transformer.feedbackTexture.renderOperation(this.uvMaskTarget, (renderer, scene, camera) => { 804 | 805 | renderer.render(transformer.uvScene, camera); 806 | 807 | } 808 | ) 809 | uvMesh.material = svMat; 810 | } 811 | 812 | //---------------end texpaint 813 | 814 | let first = true; 815 | let animating = true; 816 | let maxSubsteps = 10; 817 | 818 | let simTime = 0; 819 | let fps = 60; 820 | 821 | let takeScreenshot = false; 822 | let exportTriggered = false; 823 | 824 | let { MOUSE } = THREE; 825 | controls.mouseButtons = { 826 | LEFT: MOUSE.PAN, 827 | MIDDLE: MOUSE.PAN, 828 | RIGHT: MOUSE.ROTATE 829 | }; 830 | 831 | 832 | this.render = (time) => { 833 | controls.update(); 834 | renderer.setRenderTarget(null) 835 | renderer.render(scene, camera); 836 | } 837 | this.ScenePainter = ScenePainter; 838 | } 839 | 840 | /* 841 | let {min,max,PI} = Math; 842 | function indexedGeometryTo2dPerimeter(geom){ 843 | let arr = geom.index.array; 844 | let edges = [] 845 | for(let i=0,l=arr.length;ia[0]-b[0]); 850 | let uniqueEdges=[] 851 | for(let i=0;i")) { 12 | const primitive = selector.slice(1, -1).toLowerCase(); 13 | this.e = [createPrimitive(primitive)]; 14 | } else { 15 | this.e = document.querySelectorAll(selector); 16 | } 17 | } else if (selector instanceof HTMLElement || selector instanceof THREE.Object3D) { 18 | this.e = [selector]; 19 | } 20 | 21 | this.length = this.e.length; 22 | } 23 | 24 | position(x, y, z) { this.e.forEach(e => e.position.set(x, y, z)); return this; } 25 | scale(x, y, z) { this.e.forEach(e => e.scale.set(x, y, z)); return this; } 26 | rotation(x, y, z) { this.e.forEach(e => e.rotation.set(x, y, z)); return this; } 27 | addTo(parent) { this.e.forEach(e => parent.add(e)); return this; } 28 | attachTo(parent) { this.e.forEach(e => parent.attach(e)); return this; } 29 | 30 | // Add more methods here... 31 | } 32 | 33 | const createPrimitive = (primitive) => { 34 | switch (primitive) { 35 | case "sphere": 36 | return new THREE.Mesh(new THREE.SphereGeometry(1, 32, 32), defaultMaterial); 37 | case "plane": 38 | return new THREE.Mesh(new THREE.PlaneGeometry(1, 1), defaultMaterial); 39 | case "box": 40 | return new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), defaultMaterial); 41 | case "cylinder": 42 | return new THREE.Mesh(new THREE.CylinderGeometry(1, 1, 1, 32), defaultMaterial); 43 | case "directionallight": 44 | return new THREE.DirectionalLight(0xffffff, 1); 45 | case "pointlight": 46 | return new THREE.PointLight(0xffffff, 1, 100); 47 | // Add more cases for other primitives... 48 | default: 49 | throw new Error(`Unknown primitive: ${primitive}`); 50 | } 51 | }; 52 | 53 | const $3 = (selector) => new ThreeQuery(selector); 54 | 55 | export default $3; -------------------------------------------------------------------------------- /UnwrapUVs.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three" 2 | export default function UnwrapUVs(geometry){ 3 | 4 | let g = geometry; 5 | let triCount = (g.index?g.index.count:g.attributes.position.count)/3; 6 | console.log("autogenerating UV map... ",triCount) 7 | 8 | let box = new THREE.Box3(); 9 | let v0=new THREE.Vector3(); 10 | let v1=new THREE.Vector3(); 11 | let v2=new THREE.Vector3(); 12 | let pa = g.attributes.position.array; 13 | let getTriangle=(i)=>{ 14 | let idx = g.index.array; 15 | if(idx){ 16 | let vi=i*3; 17 | let ia=idx[vi]*3 18 | let ib=idx[vi+1]*3 19 | let ic=idx[vi+2]*3 20 | v0.set(pa[ia],pa[ia+1],pa[ia+2]) 21 | v1.set(pa[ib],pa[ib+1],pa[ib+2]) 22 | v2.set(pa[ic],pa[ic+1],pa[ic+2]) 23 | } 24 | } 25 | let boxUnwrap=(v0,v1,v2)=>{ 26 | box.setEmpty(); 27 | box.expandByPoint(v0) 28 | box.expandByPoint(v1) 29 | box.expandByPoint(v2) 30 | 31 | } 32 | let uvs=new Float32Array(pa.length*2/3); 33 | 34 | box.makeEmpty(); 35 | for(let i=0;i 2 | 3 | 4 | 5 | 6 | MonkeyPaint 7 | 8 | 9 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /monkeh.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manthrax/monkeypaint/12791abc02533eae3456af535c4970842c3f47ef/monkeh.blend -------------------------------------------------------------------------------- /monkeh.blend1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manthrax/monkeypaint/12791abc02533eae3456af535c4970842c3f47ef/monkeh.blend1 -------------------------------------------------------------------------------- /monkeh.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manthrax/monkeypaint/12791abc02533eae3456af535c4970842c3f47ef/monkeh.glb -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | START Glitch hello-app default styles 3 | 4 | The styles in this section do some minimal CSS resets, set default fonts and 5 | colors, and handle the layout for our footer and "Remix on Glitch" button. If 6 | you're new to CSS they may seem a little complicated, but you can scroll down 7 | to this section's matching END comment to see page-specific styles. 8 | ******************************************************************************/ 9 | 10 | 11 | /* 12 | The style rules specify elements by type and by attributes such as class and ID 13 | Each section indicates an element or elements, then lists the style properties to apply 14 | See if you can cross-reference the rules in this file with the elements in index.html 15 | */ 16 | 17 | /* Our default values set as CSS variables */ 18 | :root { 19 | --color-bg: #69F7BE; 20 | --color-text-main: #000000; 21 | --color-primary: #FFFF00; 22 | --wrapper-height: 87vh; 23 | --image-max-width: 300px; 24 | --image-margin: 0rem; 25 | --font-family: "HK Grotesk"; 26 | --font-family-header: "HK Grotesk"; 27 | } 28 | 29 | /* Basic page style resets */ 30 | * { 31 | box-sizing: border-box; 32 | } 33 | [hidden] { 34 | display: none !important; 35 | } 36 | 37 | body { 38 | font-family: HK Grotesk; 39 | background:black; 40 | margin:0px; 41 | padding:0px; 42 | overflow:hidden; 43 | } --------------------------------------------------------------------------------