Skip to content

Commit

Permalink
updates
Browse files Browse the repository at this point in the history
reduce metalness , increase roughness..
add primitive save/load
update readme..
  • Loading branch information
manthrax committed Apr 11, 2023
1 parent e1061d7 commit 92ed7f7
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 19 deletions.
2 changes: 1 addition & 1 deletion MonkeyPaint.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ let onWindowResize = (event)=>{
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
;


onWindowResize();
window.addEventListener("resize", onWindowResize, false);
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
# monkeypaint
An app for painting on meshes in THREEJS, with glb export.

Try it here!: https://manthrax.github.io/monkeypaint/index.html?1


This implements 3d mesh painting using via the gpu and rendertargets.

Algorithm:
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

Then the UV map is rendered a second time, with the models texture bound. The 3d vertex coordinates are also available since
the UV rendering behavior is injected into the material using shader injection.

As each UV triangle is rendered.. the vertex coordinate is transformed to world space and compared with the Brush position in worldspace
(derived from the cursor raycast)
This computes an intensity 0 to 1 for the brush affecting this texel of the UV map.
Brush color is mixed with existing texel color and output.

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)
The nearest texel that is in a UV island (within 16 pixels or so) is found , and that pixel color is output.
This step eliminates most of the seams that occur due to the filtering of textures,
by giving the islands a 16 or so pixel padding for the filtering to access on island boundaries.

Export:
intermediate rendertargets containing the painted texture, are converted to canvas texture, then run through
the binary GLTF exporter as a binary gltf file (.glb)


Save/Load/Reset:
these are a bit janky.. When you hit Save, all the brush strokes in the seesion are saved to localstorage.
so if you reload the page, and then hit Load, it will repaint the model as you had it before.
Reset clears the current localStorage save and reloads the page.

TODO:
implement custom brush shapes. right now it's just a variable sized sphere.. and doesn't orient along the ray hit normal.
Allow texture stamping as an extension of this.

Allow specifying different texture outputs for roughnessmap, metalnessmap, opacitymap, and normalmap.
186 changes: 168 additions & 18 deletions ScenePainter.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ document.title = "MonkeyPaint by thrax"
//---------------texpaint
let scenePainter
new GLTFLoader().load("monkeh.glb", (glb)=>{
//new GLTFLoader().load("CartoonTV_bake.glb", (glb)=>{
scenePainter = new ScenePainter(glb.scene);
}
);
Expand Down Expand Up @@ -50,7 +51,7 @@ function ScenePainter(paintScene) {
let query = window.location.search.substring(1).split("=");
let id = parseInt(query);
if(!((id>=0)&&(id<paintMeshes.length))) id = 2;
let sourceMesh = paintMeshes[id]
let sourceMesh = paintMeshes[id%paintMeshes.length]

sourceMesh.visible = true;

Expand Down Expand Up @@ -86,6 +87,45 @@ function ScenePainter(paintScene) {
scene.traverse(e=>e.isMesh&&e.material&&(e.material.envMapIntensity = v));
}

/*
let {min,max,PI} = Math;
function indexedGeometryTo2dPerimeter(geom){
let arr = geom.index.array;
let edges = []
for(let i=0,l=arr.length;i<l;i+=3){
let a=arr.slice(i,i+3).sort();
edges.push([a[0],a[1]],[a[1],a[2]],[a[0],a[2]])
}
edges.sort((a,b)=>a[0]-b[0]);
let uniqueEdges=[]
for(let i=0;i<edges.length;i++){
let e0=edges[i];
let e1=edges[(i+1)%edges.length];
if((e0[0]==e1[0])&&(e0[1]==e1[1]))i++; //shared edge.. discard;
else
uniqueEdges.push(e0);
}
return uniqueEdges;
}
let disc = new THREE.RingGeometry(20,10,4,1,0,PI);
//let disc = new THREE.CircleGeometry(20,10,0,PI);
let m = new THREE.Mesh(disc);
scene.add(m);
let edges = indexedGeometryTo2dPerimeter(disc);
console.log(edges);
let edgeGeom = disc.clone();
let idx = []
for(let i=0;i<edges.length;i++)idx.push(edges[i][0],edges[i][1]);
edgeGeom.setIndex(idx);
let lines = new THREE.LineSegments(edgeGeom);
scene.add(lines);
let ringPath = ()
*/




this.exportScene = ()=>{

const exporter = new GLTFExporter();
Expand Down Expand Up @@ -255,13 +295,20 @@ brushInfluence = max(0., brushInfluence - smoothstep(brushInfluence,.1,0.));
}

let drawing = false;
let buttons = 0;
document.addEventListener('pointerdown', (e)=>{
drawing = !(controls.enabled = !(cursorNode.raycast(paintMesh).length > 0));
buttons = e.buttons;
if(buttons==1){
(controls.enabled = !(cursorNode.raycast(paintMesh).length > 0));
if(!controls.enabled)
drawing = true;
}
}
);
document.addEventListener('pointerup', (e)=>{
controls.enabled = true;
drawing = false;
buttons = e.buttons;
}
)

Expand All @@ -280,21 +327,115 @@ brushInfluence = max(0., brushInfluence - smoothstep(brushInfluence,.1,0.));

renderer.setClearColor(0x101010);

let replay=[]
let replaying = false;
let replayCursor = 0;
let repeatCountdown=0;

let load=()=>{
try{
if(localStorage.monkeyReplay){
replay = JSON.parse(localStorage.monkeyReplay);
if(replay.length)replaying = true;
}
}
catch{
alert("Couldn't parse localstorage! Replay lost...")
replay = []
}
}
let getState = ()=>{return {
uBrushPoint:uBrushPoint.value.clone(),
uBrushNormal:uBrushNormal.value.clone(),
uBrushSize:uBrushSize.value.clone(),
uBrushColor : uBrushColor.value.clone(),
uBrushHardness : uBrushHardness.value,
uBrushStrength:uBrushStrength.value,
_repeat:1,
}}
let setState=(st)=>{
uBrushPoint.value.copy(st.uBrushPoint)
uBrushNormal.value.copy(st.uBrushNormal)
uBrushSize.value.copy(st.uBrushSize)
uBrushColor.value.copy(st.uBrushColor)
uBrushHardness.value=st.uBrushHardness
uBrushStrength.value=st.uBrushStrength
// console.log(st.uBrushPoint)
}
let statesEqual=(sa,sb)=>{
if(typeof sa == 'object'){
for(let f in sa)
if((!f.startsWith('_'))&&(!statesEqual(sa[f],sb[f])))
return false;
}else
return (sa==sb);
return true;
}

let lastState;
// window.onBeforeUnload=()=>{
// if(replay.length){
// localStorage.monkeyReplay = JSON.stringify(replay);
// }
// }

gui.add({save:()=>{
localStorage.monkeyReplay = JSON.stringify(replay);
console.log("Save size:",localStorage.monkeyReplay.length)
}},"save")
gui.add({load},"load")

gui.add({reset:()=>{
replay=[]
//delete localStorage.monkeyReplay;
location.reload();
}},"reset")
let updateReplay=()=>{
if(replaying){
if(replayCursor>=replay.length){
replaying = false;
lastState = undefined;
}else{
let state = replay[replayCursor];
if(!repeatCountdown)repeatCountdown=state._repeat;
else{
repeatCountdown--;
if(!repeatCountdown)replayCursor++;
}
setState(state);
}
}else{
let state = getState();
if(lastState){
if(statesEqual(state,lastState))
lastState._repeat++;
else
replay.push(lastState)
}
lastState=state;
}
}
let draw = ()=>{
let fbt = texTransformer.feedbackTexture;
let iterations = 0;
do{
try{
updateReplay();
}
catch (e){
console.log(e)
replaying = false;
replay=[]
}
uvMesh.material.map = fbt.renderTarget.texture;
texTransformer.renderUVMeshToTarget(fbt.offRenderTarget);

// texTransformer.dilateTexture();
// texTransformer.dilateTexture();

previewPlane.material.map = fbt.renderTarget.texture;
paintMesh.material.map = fbt.renderTarget.texture

uvMesh.material.map = fbt.offRenderTarget.texture;

dilator.apply(fbt);

iterations++;
}while(replaying&&(iterations<100))
dilator.apply(fbt);
}

//Convert rendertarget to canvasTexture
Expand All @@ -305,11 +446,17 @@ brushInfluence = max(0., brushInfluence - smoothstep(brushInfluence,.1,0.));
canvas.height = height;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(width, height);
const buffer = new Float32Array(width * height * 4);
renderer.readRenderTargetPixels(renderTarget, 0, 0, width, height, buffer);
for (let i = 0; i < buffer.length; i++)
buffer[i] *= 255;
imageData.data.set(buffer);
if(renderTarget.texture.type==THREE.FloatType){
const buffer = new Float32Array(width * height * 4);
renderer.readRenderTargetPixels(renderTarget, 0, 0, width, height, buffer);
for (let i = 0; i < buffer.length; i++)
buffer[i] *= 255;
imageData.data.set(buffer);
}else{
const buffer = new Uint8Array(width * height * 4);
renderer.readRenderTargetPixels(renderTarget, 0, 0, width, height, buffer);
imageData.data.set(buffer);
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Expand All @@ -326,10 +473,9 @@ brushInfluence = max(0., brushInfluence - smoothstep(brushInfluence,.1,0.));
//paintMesh.worldToLocal(n);
n.sub(v);

if (!drawing)
return;
draw();
}
if ((buttons==1)&&(drawing||replaying))
draw();
}

//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...
Expand Down Expand Up @@ -389,7 +535,7 @@ function FeedbackTexture(texture, renderer) {
let makeTarget = this.makeTarget = ()=>{
let rt = new THREE.WebGLRenderTarget(texture.image.width,texture.image.height,{
format: THREE.RGBAFormat,
type: THREE.FloatType,
type: THREE.UnsignedByteType,//THREE.FloatType,
//minFilter: THREE.NearestFilter,
//magFilter: THREE.NearestFilter,
depthBuffer: false,
Expand Down Expand Up @@ -586,6 +732,10 @@ let fps = 60;

let takeScreenshot = false;
let exportTriggered = false;

let {MOUSE} = THREE;
controls.mouseButtons = { LEFT: MOUSE.PAN, MIDDLE: MOUSE.PAN, RIGHT: MOUSE.ROTATE };

renderer.setAnimationLoop((dt)=>{

let time = performance.now();
Expand Down
Binary file modified monkeh.glb
Binary file not shown.

0 comments on commit 92ed7f7

Please sign in to comment.