@jscad/regl-renderer
Version:
Renderer for JSCAD Geometries
338 lines (294 loc) • 10.2 kB
JavaScript
const vec3 = require('gl-vec3')
const mat4 = require('gl-mat4')
const { max, min, sqrt, PI, sin, cos, atan2 } = Math
const computeBounds = require('../bound-utils/computeBounds')
// TODO: make it more data driven ?
/*
setFocus => modify the focusPoint input
rotate => modify the angle input
*/
/* cameras are assumed to have:
projection
view
target (focal point)
eye/position
up
*/
// TODO: multiple data, sometimes redundant, needs simplification
/*
- camera state
- camera props
- controls state
- controls props
- other
*/
const controlsProps = {
limits: {
minDistance: 0.01,
maxDistance: 10000
},
drag: 0.27, // Decrease the momentum by 1% each iteration
EPS: 0.000001,
zoomToFit: {
auto: true, // always tried to apply zoomTofit
targets: 'all',
tightness: 1.5 // how close should the fit be: the lower the tigher : 1 means very close, but fitting most of the time
},
// all these, not sure are needed in this shape
userControl: {
zoom: true,
zoomSpeed: 1.0,
rotate: true,
rotateSpeed: 1.0,
pan: true,
panSpeed: 1.0
},
autoRotate: {
enabled: false,
speed: 1.0 // 30 seconds per round when fps is 60
},
autoAdjustPlanes: true // adjust near & far planes when zooming in &out
}
const controlsState = {
// orbit controls state
thetaDelta: 0,
phiDelta: 0,
scale: 1
}
const defaults = Object.assign({}, controlsState, controlsProps)
const update = ({ controls, camera }, output) => {
// custom z up is settable, with inverted Y and Z (since we use camera[2] => up)
const { EPS, drag } = controls
const { position, target } = camera
const up = controls.up ? controls.up : camera.up
let curThetaDelta = controls.thetaDelta
const curPhiDelta = controls.phiDelta
const curScale = controls.scale
const offset = vec3.subtract([], position, target)
let theta
let phi
if (up[2] === 1) {
// angle from z-axis around y-axis, upVector : z
theta = atan2(offset[0], offset[1])
// angle from y-axis
phi = atan2(sqrt(offset[0] * offset[0] + offset[1] * offset[1]), offset[2])
} else {
// in case of y up
theta = atan2(offset[0], offset[2])
phi = atan2(sqrt(offset[0] * offset[0] + offset[2] * offset[2]), offset[1])
// curThetaDelta = -(curThetaDelta)
}
if (controls.autoRotate.enabled && controls.userControl.rotate) {
curThetaDelta += 2 * Math.PI / 60 / 60 * controls.autoRotate.speed
}
theta += curThetaDelta
phi += curPhiDelta
// restrict phi to be betwee EPS and PI-EPS
phi = max(EPS, min(PI - EPS, phi))
// multiply by scaling effect and restrict radius to be between desired limits
const radius = max(controls.limits.minDistance, min(controls.limits.maxDistance, vec3.length(offset) * curScale))
if (up[2] === 1) {
offset[0] = radius * sin(phi) * sin(theta)
offset[2] = radius * cos(phi)
offset[1] = radius * sin(phi) * cos(theta)
} else {
offset[0] = radius * sin(phi) * sin(theta)
offset[1] = radius * cos(phi)
offset[2] = radius * sin(phi) * cos(theta)
}
const newPosition = vec3.add(vec3.create(), target, offset)
const newView = mat4.lookAt(mat4.create(), newPosition, target, up)
const dragEffect = 1 - max(min(drag, 1.0), 0.01)
const positionChanged = vec3.distance(position, newPosition) > 0.001
/* let newMatrix = mat4.create()
newMatrix = mat4.lookAt(newMatrix, newPosition, target, up)
newMatrix = mat4.translate(matrix, matrix, newPosition) */
// update camera matrix
// let quaternion = quatFromRotationMatrix(mat4.lookAt(mat4.create(), [0, 0, 0], target, up))
// let newMatrix = composeMat4(mat4.create(), newPosition, quaternion, [1, 1, 1])
// view = newMatrix
/* if (output) {
output.controls.thetaDelta = curThetaDelta * dragEffect
} */
return {
// controls state
controls: {
thetaDelta: curThetaDelta * dragEffect,
phiDelta: curPhiDelta * dragEffect,
scale: 1,
changed: positionChanged
},
// camera state
camera: {
position: newPosition,
view: newView
}
// matrix: newMatrix
}
}
/**
* compute camera state to rotate the camera
* @param {Object} controls the controls data/state
* @param {Object} camera the camera data/state
* @param {Float} angle value of the angle to rotate
* @return {Object} the updated camera data/state
*/
const rotate = ({ controls, camera, speed = 1 }, angle) => {
let {
thetaDelta,
phiDelta
} = controls
if (controls.userControl.rotate) {
thetaDelta += (angle[0] * speed)
phiDelta += (angle[1] * speed)
}
return {
controls: {
thetaDelta,
phiDelta
},
camera
}
}
/**
* compute camera state to zoom the camera
* @param {Object} controls the controls data/state
* @param {Object} camera the camera data/state
* @param {Float} zoomDelta value of the zoom
* @return {Object} the updated camera data/state
*/
const zoom = ({ controls, camera, speed = 1 }, zoomDelta = 0) => {
let { scale } = controls
if (controls.userControl.zoom && camera && zoomDelta !== undefined && zoomDelta !== 0 && !isNaN(zoomDelta)) {
const sign = Math.sign(zoomDelta) === 0 ? 1 : Math.sign(zoomDelta)
zoomDelta = (zoomDelta / zoomDelta) * sign * speed// controls.userControl.zoomSpeed
// adjust zoom scaling based on distance : the closer to the target, the lesser zoom scaling we apply
// zoomDelta *= Math.exp(Math.max(camera.scale * 0.05, 1))
// updated scale after we will apply the new zoomDelta to the current scale
const newScale = (zoomDelta + controls.scale)
// updated distance after the scale has been updated, used to prevent going outside limits
const newDistance = vec3.distance(camera.position, camera.target) * newScale
if (newDistance > controls.limits.minDistance && newDistance < controls.limits.maxDistance) {
scale += zoomDelta
}
// for ortho cameras
if (camera.projectionType === 'orthographic') {
const distance = vec3.length(vec3.subtract([], camera.position, camera.target)) * 0.3
const width = Math.tan(camera.fov) * distance * camera.aspect
const height = Math.tan(camera.fov) * distance
const projection = require('../cameras/orthographicCamera').setProjection(camera, { width, height })
camera = projection
}
/* if (controls.autoAdjustPlanes) {
// these are empirical values , after a LOT of testing
const distance = vec3.squaredDistance(camera.target, camera.position)
camera.near = Math.min(Math.max(5, distance * 0.0015), 100)
} */
}
return { controls: { scale }, camera }
}
/**
* compute camera state to pan the camera
* @param {Object} controls the controls data/state
* @param {Object} camera the camera data/state
* @param {Float} delta value of the raw pan delta
* @return {Object} the updated camera data/state
*/
const pan = ({ controls, camera, speed = 1 }, delta) => {
const unproject = require('camera-unproject')
const { projection, view, viewport } = camera
const combinedProjView = mat4.multiply([], projection, view)
const invProjView = mat4.invert([], combinedProjView)
const panStart = [
viewport[2],
viewport[3],
0
]
const panEnd = [
viewport[2] - delta[0],
viewport[3] + delta[1],
0
]
const unPanStart = unproject([], panStart, viewport, invProjView)
const unPanEnd = unproject([], panEnd, viewport, invProjView)
const eyeDistance = vec3.distance(camera.position, camera.eye)
const offset = vec3.subtract([], unPanStart, unPanEnd).map((x) => x * speed * eyeDistance * controls.scale)
return {
controls,
camera: {
position: vec3.add(vec3.create(), camera.position, offset),
target: vec3.add(vec3.create(), camera.target, offset)
}
}
}
/**
* compute camera state to 'fit' an object on screen
* Note1: this is a non optimal but fast & easy implementation
* @param {Object} controls the controls data/state
* @param {Object} camera the camera data/state
* @param {Array} entities - an array of entities (see entitiesFromSolids)
* @return {Object} the updated camera data/state
*/
const zoomToFit = ({ controls, camera, entities }) => {
// our camera.fov is already in radian, no need to convert
const { zoomToFit } = controls
if (zoomToFit.targets !== 'all') {
return { controls, camera }
}
if (entities.length === 0) return { controls, camera }
// compute the overall bounds
const geometries = entities.map((entity) => entity.geometry)
const bounds = computeBounds(geometries)
// fixme: for now , we only use the first item
const { fov, target, position } = camera
const { tightness } = Object.assign({}, zoomToFit, controlsProps.zoomToFit)
/*
- x is scaleForIdealDistance
- currentDistance is fixed
- how many times currentDistance * x = idealDistance
So
x = idealDistance / currentDistance
*/
const idealDistanceFromCamera = (bounds.dia * tightness) / Math.tan(fov / 2.0)
const currentDistance = vec3.distance(target, position)
const scaleForIdealDistance = idealDistanceFromCamera / currentDistance
return {
camera: { target: bounds.center },
controls: { scale: scaleForIdealDistance }
}
}
/**
* compute controls state to 'reset it' to the given state
* Note1: this is a non optimal but fast & easy implementation
* @param {Object} controls the controls data/state
* @param {Object} camera the camera data/state
* @param {Object} desiredState the state to reset the camera to: defaults to default values
* @return {Object} the updated camera data/state
*/
const reset = ({ controls, camera }, desiredState) => {
const options = {
camera: {
position: desiredState.camera.position,
target: desiredState.camera.target,
projection: mat4.perspective([], camera.fov, camera.aspect, camera.near, camera.far),
view: desiredState.camera.view
},
controls: {
thetaDelta: desiredState.controls.thetaDelta,
phiDelta: desiredState.controls.phiDelta,
scale: desiredState.controls.scale
}
}
return options
}
module.exports = {
controlsProps,
controlsState,
defaults,
update,
rotate,
zoom,
pan,
zoomToFit,
reset
}