diff --git a/apps/jscad-web/main.js b/apps/jscad-web/main.js index e8ec77c..b0e181d 100644 --- a/apps/jscad-web/main.js +++ b/apps/jscad-web/main.js @@ -21,6 +21,7 @@ import * as remote from './src/remote.js' import { formatStacktrace } from './src/stacktrace.js' import { ViewState } from './src/viewState.js' import * as welcome from './src/welcome.js' +import { runMain } from '../../packages/worker/worker.js' export const byId = id => document.getElementById(id) const appBase = document.baseURI @@ -28,6 +29,7 @@ let currentBase = appBase const toUrl = path => new URL(path, appBase).toString() const viewState = new ViewState() +viewState.onRequireReRender = ()=>paramChangeCallback(lastParams) const gizmo = (window.gizmo = new Gizmo()) byId('overlay').parentNode.appendChild(gizmo) @@ -59,7 +61,7 @@ async function resetFileRefs(){ if(sw){ delete sw.fileToRun await clearFs(sw) - } + } } async function initFs() { @@ -198,7 +200,7 @@ sendCmdAndSpin('init', { }, }).then(() => { if (loadDefault) { - runScript({ script: defaultCode }) + runScript({ script: defaultCode, smooth: viewState.smoothRender }) } }) @@ -213,19 +215,19 @@ const paramChangeCallback = async params => { } working = true let result - try { - result = await sendCmdAndSpin('runMain', { params }) - } finally { + try{ + result = await sendCmdAndSpin('runMain', { params, smooth: viewState.smoothRender }) + } finally{ working = false } - handlers.entities(result) - if (lastParams && lastParams != params) paramChangeCallback(lastParams) + handlers.entities(result, {smooth: viewState.smoothRender}) + if(lastParams && lastParams != params) paramChangeCallback(lastParams) } const runScript = async ({ script, url = './jscad.model.js', base = currentBase, root }) => { currentBase = base loadDefault = false // don't load default model if something else was loaded - const result = await sendCmdAndSpin('runScript', { script, url, base, root }) + const result = await sendCmdAndSpin('runScript', { script, url, base, root, smooth: viewState.smoothRender }) genParams({ target: byId('paramsDiv'), params: result.def || {}, callback: paramChangeCallback }) handlers.entities(result) } diff --git a/apps/jscad-web/src/viewState.js b/apps/jscad-web/src/viewState.js index ba54dbe..49b7108 100644 --- a/apps/jscad-web/src/viewState.js +++ b/apps/jscad-web/src/viewState.js @@ -16,6 +16,7 @@ export class ViewState { const darkMode = byId('dark-mode') const showAxis = byId('show-axis') const showGrid = byId('show-grid') + const smoothRender = byId('smooth-render') darkMode.addEventListener('change', () => { this.themeName = darkMode.checked ? 'dark' : 'light' if (darkMode.checked) { @@ -25,6 +26,9 @@ export class ViewState { } this.setTheme(this.themeName) }) + smoothRender.addEventListener('change', () => { + this.setSmoothRender(smoothRender.checked) + }) showAxis.addEventListener('change', () => this.setAxes(showAxis.checked)) showGrid.addEventListener('change', () => this.setGrid(showGrid.checked)) } @@ -41,6 +45,12 @@ export class ViewState { this.saveState() } + setSmoothRender(smoothRender, fireEvent = true) { + this.smoothRender = smoothRender + this.saveState() + if(fireEvent) this.onRequireReRender() + } + setTheme(themeName) { if (!themes[themeName]) throw new Error(`unknown theme ${themeName}`) this.themeName = themeName @@ -86,7 +96,7 @@ export class ViewState { if (grid) items.push({ id: 'grid', items: grid }) if (model) items.push({ id: 'model', items: model }) - this.viewer?.setScene({ items }) + this.viewer?.setScene({ items }, {smooth:this.smoothRender}) } setEngine(viewer) { @@ -108,6 +118,8 @@ export class ViewState { byId('show-axis').checked = this.showAxis this.showGrid = localStorage.getItem('engine.showGrid') !== 'false' byId('show-grid').checked = this.showGrid + this.smoothRender = !!localStorage.getItem('engine.smoothRender') + byId('smooth-render').checked = this.smoothRender const cameraLocation = localStorage.getItem('camera.location') this.camera = cameraLocation ? JSON.parse(cameraLocation) : { position: [180, -180, 220] } } @@ -116,5 +128,8 @@ export class ViewState { localStorage.setItem('engine.theme', this.themeName) localStorage.setItem('engine.showAxis', this.showAxis) localStorage.setItem('engine.showGrid', this.showGrid) + localStorage.setItem('engine.smoothRender', this.smoothRender) } + + onRequireReRender(){} } diff --git a/apps/jscad-web/static/index.html b/apps/jscad-web/static/index.html index 8644dc0..1aa9471 100644 --- a/apps/jscad-web/static/index.html +++ b/apps/jscad-web/static/index.html @@ -82,6 +82,7 @@

Options

  • +
  • Documentation

    diff --git a/packages/format-threejs/index.js b/packages/format-threejs/index.js index 3a74d1b..8418cf9 100644 --- a/packages/format-threejs/index.js +++ b/packages/format-threejs/index.js @@ -1,4 +1,4 @@ -export function CommonToThree ({ +export function CommonToThree({ MeshPhongMaterial, LineBasicMaterial, BufferGeometry, @@ -7,26 +7,25 @@ export function CommonToThree ({ InstancedMesh, Line, LineSegments, - Color + Color, + Vector3, }) { - - const flatShading = true + const flatShading = false const materials = { mesh: { def: new MeshPhongMaterial({ color: 0x0084d1, flatShading }), - make: params => new MeshPhongMaterial({ flatShading, ...params }) + make: params => new MeshPhongMaterial({ flatShading, ...params }), }, line: { def: new LineBasicMaterial({ color: 0x0000ff }), - make: params => - new LineBasicMaterial(params) + make: params => new LineBasicMaterial(params), }, - lines: null + lines: null, } materials.lines = materials.line - materials.instance = materials.mesh// todo support instances for lines + materials.instance = materials.mesh // todo support instances for lines - function _CSG2Three (obj) { + function _CSG2Three(obj, { smooth = false }) { const { vertices, indices, normals, color, colors, isTransparent = false, opacity } = obj let { transforms } = obj const objType = obj.type || 'mesh' @@ -43,7 +42,7 @@ export function CommonToThree ({ const opts = { vertexColors: !!colors, opacity: c[3] === undefined ? 1 : c[3], - transparent: (color && c[3] !== 1 && c[3] !== undefined) || isTransparent + transparent: (color && c[3] !== 1 && c[3] !== undefined) || isTransparent, } if (opacity) opts.opacity = opacity if (!colors) opts.color = _CSG2Three.makeColor(color) @@ -54,10 +53,11 @@ export function CommonToThree ({ } } - const geo = new BufferGeometry() + let geo = new BufferGeometry() geo.setAttribute('position', new BufferAttribute(vertices, 3)) if (indices) geo.setIndex(new BufferAttribute(indices, 1)) if (normals) geo.setAttribute('normal', new BufferAttribute(normals, 3)) + if(smooth) geo = toCreasedNormals({ Vector3, BufferAttribute }, geo, Math.PI / 10) if (colors) geo.setAttribute('color', new BufferAttribute(colors, isTransparent ? 4 : 3)) let mesh @@ -65,15 +65,11 @@ export function CommonToThree ({ case 'mesh': mesh = new Mesh(geo, material) break - case 'instance': - const {list} = obj - mesh = new InstancedMesh( - geo, - materials.mesh.make({ color: 0x0084d1 }), - list.length - ) - list.forEach((item, i)=>{ - copyTransformToArray(item.transforms, mesh.instanceMatrix.array,i*16) + case 'instance': + const { list } = obj + mesh = new InstancedMesh(geo, materials.mesh.make({ color: 0x0084d1 }), list.length) + list.forEach((item, i) => { + copyTransformToArray(item.transforms, mesh.instanceMatrix.array, i * 16) }) transforms = null break @@ -90,37 +86,137 @@ export function CommonToThree ({ } // shortcut for setMatrixAt for InstancedMesh - function copyTransformToArray( te, array = [], offset = 0 ) { - - array[ offset ] = te[ 0 ]; - array[ offset + 1 ] = te[ 1 ]; - array[ offset + 2 ] = te[ 2 ]; - array[ offset + 3 ] = te[ 3 ]; + function copyTransformToArray(te, array = [], offset = 0) { + array[offset] = te[0] + array[offset + 1] = te[1] + array[offset + 2] = te[2] + array[offset + 3] = te[3] - array[ offset + 4 ] = te[ 4 ]; - array[ offset + 5 ] = te[ 5 ]; - array[ offset + 6 ] = te[ 6 ]; - array[ offset + 7 ] = te[ 7 ]; + array[offset + 4] = te[4] + array[offset + 5] = te[5] + array[offset + 6] = te[6] + array[offset + 7] = te[7] - array[ offset + 8 ] = te[ 8 ]; - array[ offset + 9 ] = te[ 9 ]; - array[ offset + 10 ] = te[ 10 ]; - array[ offset + 11 ] = te[ 11 ]; + array[offset + 8] = te[8] + array[offset + 9] = te[9] + array[offset + 10] = te[10] + array[offset + 11] = te[11] - array[ offset + 12 ] = te[ 12 ]; - array[ offset + 13 ] = te[ 13 ]; - array[ offset + 14 ] = te[ 14 ]; - array[ offset + 15 ] = te[ 15 ]; + array[offset + 12] = te[12] + array[offset + 13] = te[13] + array[offset + 14] = te[14] + array[offset + 15] = te[15] - return array; - - } + return array + } - _CSG2Three.makeColor = (c) => new Color(c[0], c[1], c[2]) + _CSG2Three.makeColor = c => new Color(c[0], c[1], c[2]) _CSG2Three.materials = materials - _CSG2Three.setDefColor = (c)=>{ - materials.mesh.def = new MeshPhongMaterial({color:_CSG2Three.makeColor(c), flatShading}) + _CSG2Three.setDefColor = c => { + materials.mesh.def = new MeshPhongMaterial({ color: _CSG2Three.makeColor(c), flatShading }) } return _CSG2Three } + +/** from threejs examples BufferGeometryUtils + * @param {BufferGeometry} geometry + * @param {number} tolerance + * @return {BufferGeometry>} + */ + +/** + * Modifies the supplied geometry if it is non-indexed, otherwise creates a new, + * non-indexed geometry. Returns the geometry with smooth normals everywhere except + * faces that meet at an angle greater than the crease angle. + * + * @param {BufferGeometry} geometry + * @param {number} [creaseAngle] + * @return {BufferGeometry} + */ +function toCreasedNormals({ Vector3, BufferAttribute }, geometry, creaseAngle = Math.PI / 3 /* 60 degrees */) { + const creaseDot = Math.cos(creaseAngle) + const hashMultiplier = (1 + 1e-10) * 1e2 + + // reusable vectors + const verts = [new Vector3(), new Vector3(), new Vector3()] + const tempVec1 = new Vector3() + const tempVec2 = new Vector3() + const tempNorm = new Vector3() + const tempNorm2 = new Vector3() + + // hashes a vector + function hashVertex(v) { + const x = ~~(v.x * hashMultiplier) + const y = ~~(v.y * hashMultiplier) + const z = ~~(v.z * hashMultiplier) + return `${x},${y},${z}` + } + + // BufferGeometry.toNonIndexed() warns if the geometry is non-indexed + // and returns the original geometry + const resultGeometry = geometry.index ? geometry.toNonIndexed() : geometry + const posAttr = resultGeometry.attributes.position + const vertexMap = {} + + // find all the normals shared by commonly located vertices + for (let i = 0, l = posAttr.count / 3; i < l; i++) { + const i3 = 3 * i + const a = verts[0].fromBufferAttribute(posAttr, i3 + 0) + const b = verts[1].fromBufferAttribute(posAttr, i3 + 1) + const c = verts[2].fromBufferAttribute(posAttr, i3 + 2) + + tempVec1.subVectors(c, b) + tempVec2.subVectors(a, b) + + // add the normal to the map for all vertices + const normal = new Vector3().crossVectors(tempVec1, tempVec2).normalize() + for (let n = 0; n < 3; n++) { + const vert = verts[n] + const hash = hashVertex(vert) + if (!(hash in vertexMap)) { + vertexMap[hash] = [] + } + + vertexMap[hash].push(normal) + } + } + + // average normals from all vertices that share a common location if they are within the + // provided crease threshold + const normalArray = new Float32Array(posAttr.count * 3) + const normAttr = new BufferAttribute(normalArray, 3, false) + for (let i = 0, l = posAttr.count / 3; i < l; i++) { + // get the face normal for this vertex + const i3 = 3 * i + const a = verts[0].fromBufferAttribute(posAttr, i3 + 0) + const b = verts[1].fromBufferAttribute(posAttr, i3 + 1) + const c = verts[2].fromBufferAttribute(posAttr, i3 + 2) + + tempVec1.subVectors(c, b) + tempVec2.subVectors(a, b) + + tempNorm.crossVectors(tempVec1, tempVec2).normalize() + + // average all normals that meet the threshold and set the normal value + for (let n = 0; n < 3; n++) { + const vert = verts[n] + const hash = hashVertex(vert) + const otherNormals = vertexMap[hash] + tempNorm2.set(0, 0, 0) + + for (let k = 0, lk = otherNormals.length; k < lk; k++) { + const otherNorm = otherNormals[k] + if (tempNorm.dot(otherNorm) > creaseDot) { + tempNorm2.add(otherNorm) + } + } + + tempNorm2.normalize() + normAttr.setXYZ(i3 + n, tempNorm2.x, tempNorm2.y, tempNorm2.z) + } + } + + resultGeometry.setAttribute('normal', normAttr) + return resultGeometry +} diff --git a/packages/render-threejs/index.js b/packages/render-threejs/index.js index d3af7a4..d66513e 100644 --- a/packages/render-threejs/index.js +++ b/packages/render-threejs/index.js @@ -24,6 +24,7 @@ export function RenderThreejs({ let _camera let controls let renderer + let smooth const SHADOW = false const shouldRender = Date.now() const lastRender = true @@ -44,9 +45,10 @@ export function RenderThreejs({ Line, LineSegments, Color, + Vector3, }) - const startRenderer = ({ canvas, cameraPosition = [180, -180, 220], cameraTarget = [0, 0, 0], bg = [1, 1, 1] }) => { + const startRenderer = ({ canvas, cameraPosition = [180, -180, 220], cameraTarget = [0, 0, 0], bg = [1, 1, 1], lightPosition}) => { _camera = new PerspectiveCamera(45, 1, 1, 50000) _camera.up.set(0, 0, 1) _camera.position.set(...cameraPosition) @@ -56,16 +58,15 @@ export function RenderThreejs({ window.updateView = updateView _scene = new Scene() - - const ambientLight = new AmbientLight(0xeeeeee, 0.2) + lightPosition = null + const ambientLight = new AmbientLight(0xeeeeee, lightPosition ? 0.2 : 0.5) _scene.add(ambientLight) const hemiLight = new HemisphereLight(0xeeeedd, 0x333333, 0.5) hemiLight.position.set(0, 0, 2000) - _scene.add(hemiLight) + if(lightPosition) _scene.add(hemiLight) const directionalLight = new DirectionalLight(0xeeeef4, 0.7) - directionalLight.position.set(0, -200, 100) directionalLight.castShadow = SHADOW if (SHADOW) { directionalLight.shadow.camera.top = 180 @@ -73,8 +74,15 @@ export function RenderThreejs({ directionalLight.shadow.camera.left = -120 directionalLight.shadow.camera.right = 120 } - _scene.add(directionalLight) - + if(lightPosition){ + directionalLight.position.set(...lightPosition) + _scene.add(directionalLight) + }else{ + // set pos relative to camera + directionalLight.position.set(50,0,100) + _camera.add(directionalLight) + _scene.add(_camera) + } setBg(bg) renderer = new WebGLRenderer({ antialias: true, preserveDrawingBuffer: true, canvas }) @@ -93,6 +101,10 @@ export function RenderThreejs({ updateView() } + function setSmooth(v){ + smooth = v + } + function setMeshColor(bg = [1, 1, 1]) { meshColor = new Color(...bg) csgConvert.setDefColor(bg) @@ -186,7 +198,8 @@ export function RenderThreejs({ } } - function setScene(scene) { + function setScene(scene,{smooth}={}) { + console.warn('options', {smooth}) groups.forEach(group => { _scene.remove(group) }) @@ -198,7 +211,7 @@ export function RenderThreejs({ const group = new Group() groups.push(group) item.items.forEach(obj => { - const obj3d = csgConvert(obj, scene, meshColor) + const obj3d = csgConvert(obj, { smooth, scene, meshColor}) if (obj3d) { entities.push(obj3d) group.add(obj3d)