diff --git a/package-lock.json b/package-lock.json index 10feb6cd62..1781de25ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3506,8 +3506,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.2.tgz", "integrity": "sha512-RwywHlpCRc3/Wh81MiCKun4ydaIFyW5Ea6JbL6sRCVx5q5irDw7pMXBUFYF/jArQ6YrG36q0kpovc9P/Kd3I4g==", - "dev": true, - "optional": true + "dev": true }, "boom": { "version": "4.3.1", @@ -14877,6 +14876,11 @@ "symbol-observable": "^1.2.0" } }, + "redux-batched-actions": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/redux-batched-actions/-/redux-batched-actions-0.5.0.tgz", + "integrity": "sha512-6orZWyCnIQXMGY4DUGM0oj0L7oYnwTACsfsru/J7r94RM3P9eS7SORGpr3LCeRCMoIMQcpfKZ7X4NdyFHBS8Eg==" + }, "redux-promise": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/redux-promise/-/redux-promise-0.5.3.tgz", @@ -18243,8 +18247,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true, - "optional": true + "dev": true }, "binary-extensions": { "version": "1.13.1", @@ -18258,7 +18261,6 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, - "optional": true, "requires": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", @@ -18277,7 +18279,6 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, - "optional": true, "requires": { "is-extendable": "^0.1.0" } @@ -18310,7 +18311,6 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "dev": true, - "optional": true, "requires": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -18321,7 +18321,6 @@ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, - "optional": true, "requires": { "is-plain-object": "^2.0.4" } @@ -18333,7 +18332,6 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, - "optional": true, "requires": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", @@ -18346,7 +18344,6 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, - "optional": true, "requires": { "is-extendable": "^0.1.0" } @@ -18402,7 +18399,6 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, - "optional": true, "requires": { "kind-of": "^3.0.2" }, @@ -18412,7 +18408,6 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, - "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -18423,15 +18418,13 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "optional": true + "dev": true }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, - "optional": true, "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -18472,7 +18465,6 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dev": true, - "optional": true, "requires": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" diff --git a/package.json b/package.json index 66ac574a70..1dfade8ddb 100644 --- a/package.json +++ b/package.json @@ -243,6 +243,7 @@ "react-use-gesture": "^6.0.14", "react-window": "1.5.0", "redux": "4.0.1", + "redux-batched-actions": "^0.5.0", "redux-promise": "0.5.3", "redux-thunk": "2.3.0", "redux-undo": "1.0.0-beta9-9-7", diff --git a/src/js/shot-explorer/Character.js b/src/js/shot-explorer/Character.js index 8d6062ce95..2e385d3561 100644 --- a/src/js/shot-explorer/Character.js +++ b/src/js/shot-explorer/Character.js @@ -7,6 +7,7 @@ import {useAsset} from '../shot-generator/hooks/use-assets-manager' import { SHOT_LAYERS } from '../shot-generator/utils/ShotLayers' import {patchMaterial} from '../shot-generator/helpers/outlineMaterial' import isUserModel from '../shot-generator/helpers/isUserModel' +import isSuitableForIk from './utils/isSuitableForIk' import FaceMesh from "../shot-generator/components/Three/Helpers/FaceMesh" const Character = React.memo(({ path, sceneObject, modelSettings, ...props}) => { @@ -19,6 +20,7 @@ const Character = React.memo(({ path, sceneObject, modelSettings, ...props}) => } const {asset: gltf} = useAsset(path) + const isIkCharacter = useRef() const ref = useUpdate( self => { let lod = self.getObjectByProperty("type", "LOD") || self @@ -82,6 +84,7 @@ const Character = React.memo(({ path, sceneObject, modelSettings, ...props}) => let skeleton = lod.children[0].skeleton skeleton.pose() + isIkCharacter.current = isSuitableForIk(skeleton) getFaceMesh().setSkinnedMesh(lod, gl) let originalSkeleton = skeleton.clone() originalSkeleton.bones = originalSkeleton.bones.map(bone => bone.clone()) @@ -221,7 +224,8 @@ const Character = React.memo(({ path, sceneObject, modelSettings, ...props}) => height: originalHeight, locked: locked, name: sceneObject.displayName, - modelName: sceneObject.model + modelName: sceneObject.model, + isSameSkeleton: isIkCharacter.current }} position={ [x, z, y] } diff --git a/src/js/shot-explorer/ShotElement.js b/src/js/shot-explorer/ShotElement.js index ec21d92dfd..76d5710487 100644 --- a/src/js/shot-explorer/ShotElement.js +++ b/src/js/shot-explorer/ShotElement.js @@ -15,6 +15,9 @@ const ShotElement = React.memo(( const updateImage = () => { setImageChanged({}) } + useEffect(() => { + return () => object.unsubscribe(updateImage) + }, []) useEffect(() => { object.subscribe(updateImage) diff --git a/src/js/shot-explorer/ShotExplorerSceneManager.js b/src/js/shot-explorer/ShotExplorerSceneManager.js index 48ee36428c..8837150bcb 100644 --- a/src/js/shot-explorer/ShotExplorerSceneManager.js +++ b/src/js/shot-explorer/ShotExplorerSceneManager.js @@ -59,7 +59,7 @@ const ShotExplorerSceneManager = connect( models, setLargeCanvasData, withState, - shouldRender, + shouldRender }) => { const { scene, camera, gl } = useThree() const rootRef = useRef() @@ -94,7 +94,7 @@ const ShotExplorerSceneManager = connect( useEffect(() => { setLargeCanvasData(camera, scene, gl) - }, [scene.__interaction.length, camera, gl]) + }, [scene, camera, gl]) const groundTexture = useTextureLoader(window.__dirname + '/data/shot-generator/grid_floor_1.png') const roomTexture = useTextureLoader(window.__dirname + '/data/shot-generator/grid_wall2.png') diff --git a/src/js/shot-explorer/ShotMaker.js b/src/js/shot-explorer/ShotMaker.js index ec0e85f4b3..828172c955 100644 --- a/src/js/shot-explorer/ShotMaker.js +++ b/src/js/shot-explorer/ShotMaker.js @@ -17,9 +17,30 @@ import ObjectTween from './objectTween' import ShotElement from './ShotElement' import InfiniteScroll from './InfiniteScroll' import generateRule from './ShotsRule/RulesGenerator' -import isUserModel from '../shot-generator/helpers/isUserModel' import getRandomNumber from './utils/getRandomNumber' +import {cache} from '../shot-generator/hooks/use-assets-manager' +const shotSizes = [ + { value: ShotSizes.EXTREME_CLOSE_UP, label: "Extreme Close Up" }, + { value: ShotSizes.VERY_CLOSE_UP, label: "Very Close Up" }, + { value: ShotSizes.CLOSE_UP, label: "Close Up" }, + { value: ShotSizes.MEDIUM_CLOSE_UP, label: "Medium Close Up" }, + { value: ShotSizes.BUST, label: "Bust" }, + { value: ShotSizes.MEDIUM, label: "Medium Shot" }, + { value: ShotSizes.MEDIUM_LONG, label: "Medium Long Shot" }, + { value: ShotSizes.LONG, label: "Long Shot / Wide" }, + { value: ShotSizes.EXTREME_LONG, label: "Extreme Long Shot" }, + { value: ShotSizes.ESTABLISHING, label: "Establishing Shot" } +] + +const shotAngles = [ + { value: ShotAngles.BIRDS_EYE, label: "Bird\'s Eye" }, + { value: ShotAngles.HIGH, label: "High" }, + { value: ShotAngles.EYE, label: "Eye" }, + { value: ShotAngles.LOW, label: "Low" }, + { value: ShotAngles.WORMS_EYE, label: "Worm\'s Eye" } +] + import { useTranslation } from 'react-i18next' const getRandomFov = (aspectRatio) => { @@ -39,7 +60,8 @@ const ShotMaker = React.memo(({ withState, aspectRatio, newAssetsLoaded, - canvasHeight + canvasHeight, + sceneObjects }) => { const camera = useRef() const [selectedShot, selectShot] = useState(null) @@ -51,6 +73,7 @@ const ShotMaker = React.memo(({ const [windowHeight, setWindowHeight] = useState(window.innerWidth) const [windowWidth, setWindowWidth] = useState(window.innerWidth) const containerHeight = useRef() + const [assetsLoaded, setAssetsLoaded] = useState() const { t } = useTranslation() const handleResize = () => { let container = document.getElementsByClassName("shots-container") @@ -75,13 +98,31 @@ const ShotMaker = React.memo(({ }) selectShot(newSelectedShot) } + + const isAnyAssetsPending = () => { + let assets = Object.values(cache.get()) + for(let i = 0; i < assets.length; i++) { + if(assets[i].status === "PENDING") return true + } + return false + } + + const updateAssets = (event) => { + // console.log("Trying to update assets") + if(!isAnyAssetsPending()) { + setAssetsLoaded({}) + } + } + useEffect(() => { if (!imageRenderer.current) { imageRenderer.current = new THREE.WebGLRenderer({ antialias: true }) } outlineEffect.current = new OutlineEffect(imageRenderer.current, { defaultThickness: 0.015 }) + cache.subscribe(updateAssets) handleResize() return () => { + cache.unsubscribe(updateAssets) imageRenderer.current = null outlineEffect.current = null cleanUpShots() @@ -96,29 +137,27 @@ const ShotMaker = React.memo(({ const convertCanvasToImage = async (outlineEffect, scene, camera) => { return new Promise((resolve, reject) => { - setTimeout(() => { - outlineEffect.render(scene, camera) - let image = outlineEffect.domElement.toDataURL('image/jpeg', 0.5) - resolve(image); - }, 10) + outlineEffect.render(scene, camera) + let image = outlineEffect.domElement.toDataURL('image/jpeg', 0.5) + resolve(image); }) } const renderSceneWithCamera = useCallback((shotsArray) => { let width = Math.ceil(900 * aspectRatio) outlineEffect.current.setSize(width, 900) + for(let i = 0; i < shotsArray.length; i++) { let shot = shotsArray[i] convertCanvasToImage(outlineEffect.current, sceneInfo.scene, shot.camera).then((cameraImage) => { // NOTE() : a bad practice to update component but it's okay for now - shot.setRenderImage( cameraImage ) + shot.setRenderImage( cameraImage ) }) } - }, [sceneInfo]) const generateShot = (shotsArray, shotsCount) => { - let characters = sceneInfo.scene.__interaction.filter(object => object.userData.type === 'character' && !isUserModel(object.userData.modelName)) + let characters = sceneInfo.scene.__interaction.filter(object => object.userData.type === 'character' && object.userData.isSameSkeleton) if(!characters.length) { setNoCharacterWarn(true) return; @@ -127,16 +166,16 @@ const ShotMaker = React.memo(({ } for(let i = 0; i < shotsCount; i++) { let cameraCopy = camera.current.clone() - let shotAngleKeys = Object.keys(ShotAngles) - let randomAngle = ShotAngles[shotAngleKeys[getRandomNumber(shotAngleKeys.length)]] + let shotAngleKeys = Object.keys(shotAngles) + let randomAngle = shotAngles[shotAngleKeys[getRandomNumber(shotAngleKeys.length)]] - let shotSizeKeys = Object.keys(ShotSizes) - let randomSize = ShotSizes[shotSizeKeys[getRandomNumber(shotSizeKeys.length - 2)]] + let shotSizeKeys = Object.keys(shotSizes) + let randomSize = shotSizes[shotSizeKeys[getRandomNumber(shotSizeKeys.length)]] let character = characters[getRandomNumber(characters.length)] let skinnedMesh = character.getObjectByProperty("type", "SkinnedMesh") - if(!skinnedMesh) continue - let shot = new ShotItem(randomAngle, randomSize, character) + if(!skinnedMesh || !skinnedMesh.skeleton) continue + let shot = new ShotItem(randomAngle.label, randomSize.label, character) cameraCopy.fov = getRandomFov(aspectRatio) cameraCopy.updateProjectionMatrix() let box = setShot({camera: cameraCopy, characters, selected:character, shotAngle:shot.angle, shotSize:shot.size}) @@ -145,23 +184,21 @@ const ShotMaker = React.memo(({ // Calculates box center in order to calculate camera height let center = new THREE.Vector3() box.getCenter(center) - - // Generates random rule for shot - shot.rules = generateRule(center, character, shot, cameraCopy, skinnedMesh) + // Generates random rule for shot + shot.rules = generateRule(center, character, shot, cameraCopy, skinnedMesh, characters) // Removes applying rule to Establishing, cause Establishing take in cosiderationg multiple chracters while // rule is designed to apply to one character if(ShotSizes.ESTABLISHING !== shot.size) { for(let i = 0; i < shot.rules.length; i++) { - shot.rules[i].applyRule() + shot.rules[i].applyRule(sceneInfo.scene) } } shot.camera = cameraCopy shotsArray.push(shot) } } - - useEffect(() => { + const generateShots = () => { if(sceneInfo) { camera.current = sceneInfo.camera.clone() withState((dispatch, state) => { @@ -187,13 +224,18 @@ const ShotMaker = React.memo(({ let shotsArray = [] let shotsCount = Math.ceil(containerHeight.current / (height + 20)) * 3 generateShot(shotsArray, shotsCount) - renderSceneWithCamera(shotsArray) shotsArray[0] && setSelectedShot(shotsArray[0]) cleanUpShots() setShots(shotsArray) } - }, [sceneInfo, newAssetsLoaded]) + } + + useEffect(() => { + if(!isAnyAssetsPending()) { + generateShots() + } + }, [sceneInfo, sceneObjects, assetsLoaded]) const generateMoreShots = useCallback(() => { let shotsArray = [] diff --git a/src/js/shot-explorer/ShotsRule/AreaShotRule.js b/src/js/shot-explorer/ShotsRule/AreaShotRule.js new file mode 100644 index 0000000000..e433b51571 --- /dev/null +++ b/src/js/shot-explorer/ShotsRule/AreaShotRule.js @@ -0,0 +1,96 @@ +import ShotRule from "./ShotRule" +import * as THREE from 'three' + +const isBoneInShot = (bone) => { + let name = bone.name; + return name === "Neck" || name === "Head" || name === "leaf" + || name === "LeftEye" || name === "RightEye" || name === "LeftShoulder" + || name === "RightShoulder"; +} + +class AreaShotRule extends ShotRule { + constructor(focusedCenter, camera, characters, shot) { + super(focusedCenter, camera); + this.characters = characters; + this.shot = shot; + this.radius = 1.5; + } + + applyRule(scene) { + super.applyRule(); + let character = this.shot.character; + let charactersInRange = []; + let characterPosition = character.worldPosition(); + let headPoints = []; + let frustum = new THREE.Frustum(); + this.camera.updateMatrixWorld(true); + this.camera.updateProjectionMatrix(); + + frustum.setFromProjectionMatrix( new THREE.Matrix4().multiplyMatrices( this.camera.projectionMatrix, this.camera.matrixWorldInverse ) ); + for( let i = 0; i < this.characters.length; i++) { + let position = this.characters[i].worldPosition(); + if(Math.pow(position.x - characterPosition.x, 2) + Math.pow(position.y - characterPosition.y, 2) < Math.pow(this.radius, 2)) { + let shotCharacter = this.characters[i]; + let skinnedMesh = shotCharacter.getObjectByProperty("type", "SkinnedMesh"); + if(!skinnedMesh || !skinnedMesh.skeleton) continue + let box = new THREE.Box3(); + for(let i = 0; i < skinnedMesh.skeleton.bones.length; i++) { + let bone = skinnedMesh.skeleton.bones[i]; + box.expandByPoint(bone.worldPosition()); + } + if(skinnedMesh && frustum.intersectsBox(box)) { + charactersInRange.push(shotCharacter); + headPoints = headPoints.concat(this.getCharacterShotPoints(skinnedMesh)); + } + } + } + let box = new THREE.Box3().setFromPoints(headPoints); + if(charactersInRange.length > 1) { + + //#region Camera distancing method + let center = this.focusedCenter; + let areaCenter = new THREE.Vector3(); + box.getCenter(areaCenter) + areaCenter.y = box.max.y + areaCenter.x = center.x + areaCenter.z = center.z + let BA = new THREE.Vector3().subVectors(center, this.camera.position); + let BC = new THREE.Vector3().subVectors(areaCenter, this.camera.position); + let cosineAngle = BA.dot(BC) / (BA.length() * BC.length()); + let angle = Math.acos(cosineAngle); + + let difference = center.clone().sub(areaCenter); + let normalCenter = difference.clone().normalize(); + normalCenter.set(normalCenter.y, normalCenter.x, 0); + let sphere = new THREE.Sphere(); + box.getBoundingSphere(sphere); + this.camera.rotateOnAxis(normalCenter, angle); + let direction = new THREE.Vector3(); + this.camera.getWorldDirection(direction); + direction.negate(); + let depth = sphere.radius / Math.tan(this.camera.fov / 2 * Math.PI / 180.0); + let newPos = new THREE.Vector3().addVectors(sphere.center, direction.clone().setLength(depth)); + if(sphere.center.distanceTo(newPos) + this.radius > sphere.center.distanceTo(this.camera.position)) { + this.camera.position.copy(newPos) + } + this.camera.updateMatrixWorld(true); + //#endregion + + } + return null + } + + + //#region private methods + getCharacterShotPoints(skinnedMesh) { + let headPoints = []; + let shotBones = skinnedMesh.skeleton.bones.filter(bone => isBoneInShot(bone)); + for(let i = 0; i < shotBones.length; i++) { + headPoints.push(shotBones[i].worldPosition()); + } + return headPoints; + } + //#endregion +} + +export default AreaShotRule; diff --git a/src/js/shot-explorer/ShotsRule/HorizontalOneThirdRule.js b/src/js/shot-explorer/ShotsRule/HorizontalOneThirdRule.js index 8cbfd8374f..a4c1ccb963 100644 --- a/src/js/shot-explorer/ShotsRule/HorizontalOneThirdRule.js +++ b/src/js/shot-explorer/ShotsRule/HorizontalOneThirdRule.js @@ -17,15 +17,18 @@ class HorizontalOneThirdRule extends ShotRule { let desiredPos = new THREE.Vector3(centerOfView.x, y, centerOfView.z); let minHeight = centerOfView.y - height / 2; desiredPos.y += minHeight; - + center = center.clone() // Calculates angle between two vectors let BA = new THREE.Vector3().subVectors(center, this.camera.position) let BC = new THREE.Vector3().subVectors(desiredPos, this.camera.position) let cosineAngle = BA.dot(BC) / (BA.length() * BC.length()); let angle = Math.acos(cosineAngle); - angle = desiredPos.y > center.y ? -angle : angle; this.cameraRotation = angle; - this.camera.rotateX(angle); + let normalCenter = center.clone().sub(desiredPos).normalize() + normalCenter.x += normalCenter.y + normalCenter.y = normalCenter.x - normalCenter.y + normalCenter.x = normalCenter.x - normalCenter.y + this.camera.rotateOnAxis(normalCenter, angle) this.camera.updateMatrixWorld(true); } } diff --git a/src/js/shot-explorer/ShotsRule/RulesGenerator.js b/src/js/shot-explorer/ShotsRule/RulesGenerator.js index 6480ed9ea3..acafaef01f 100644 --- a/src/js/shot-explorer/ShotsRule/RulesGenerator.js +++ b/src/js/shot-explorer/ShotsRule/RulesGenerator.js @@ -3,6 +3,7 @@ import RollRule from "./RollRule" import * as THREE from 'three' import HorizontalOneThirdRule from './HorizontalOneThirdRule' import OrbitingRule from './OrbitingRule' +import AreaShotRule from './AreaShotRule' // Clamps rotation so it's stay in -180 and 180 degrees range const clamRotationTo = (rotation, clampDegree = 180) => { if(rotation === clampDegree || rotation === -clampDegree) return rotation @@ -19,21 +20,27 @@ const getRandomNumber = (maxLength) => { return number; } -const generateRule = (focusedCenter, character, shot, camera, skinnedMesh) => { +const generateRule = (focusedCenter, character, shot, camera, skinnedMesh, characters, isCustomModel = false) => { let i = getRandomNumber(100); let results = []; //#region Finds Headbone and it's children and calculates their center for vertical oneThird + let headCenter = new THREE.Vector3() let headBone = skinnedMesh.skeleton.bones.filter(bone => bone.name === "Head")[0] - let headPoints = [] - headPoints.push(headBone.worldPosition()) - for(let i = 0; i < headBone.children.length; i++) { - if(headBone.children[i].name.includes('leaf')) - headPoints.push(headBone.children[i].worldPosition()) + if(!isCustomModel) { + let headPoints = [] + headPoints.push(headBone.worldPosition()) + for(let i = 0; i < headBone.children.length; i++) { + if(headBone.children[i].name.includes('leaf')) + headPoints.push(headBone.children[i].worldPosition()) + } + let headBox = new THREE.Box3().setFromPoints(headPoints) + + headBox.getCenter(headCenter) + } else { + headCenter.copy(focusedCenter) } - let headBox = new THREE.Box3().setFromPoints(headPoints) - let headCenter = new THREE.Vector3() - headBox.getCenter(headCenter) + //#endregion // Chance to apply orbiting rule is 100%. Orbiting should be always applied if(i < 100) { @@ -49,11 +56,16 @@ const generateRule = (focusedCenter, character, shot, camera, skinnedMesh) => { // Chance to apply vertical one third rule is 70%. VerticalOneThirdRule is left/right framing rule // rotates camera to left or right so that character stays in one third part of scene if(i < 70) { - let headQuaternion = headBone.worldQuaternion() - let rotation = new THREE.Euler().setFromQuaternion(headQuaternion) + let headQuaternion = new THREE.Quaternion() + if(!isCustomModel) { + headBone.getWorldQuaternion(headQuaternion) + } else { + character.getWorldQuaternion(headQuaternion) + } + let rotation = new THREE.Euler().setFromQuaternion(headQuaternion, "YXZ") let characterRotation = rotation.y * THREE.Math.RAD2DEG let cameraRotation = shot.cameraRotation ? shot.cameraRotation * THREE.Math.RAD2DEG : 0 - let characterFacingRotation = cameraRotation - (characterRotation) + let characterFacingRotation = cameraRotation - characterRotation characterFacingRotation = clamRotationTo(characterFacingRotation) results.push(new VerticalOneThirdRule(focusedCenter, camera, headCenter, characterFacingRotation < 0 ? "left" : "right")); } @@ -62,6 +74,11 @@ const generateRule = (focusedCenter, character, shot, camera, skinnedMesh) => { if(i < 100) { results.push( new HorizontalOneThirdRule(headCenter, camera, focusedCenter)) } + if(i < 10) { + let areaShotRule = new AreaShotRule(headCenter, camera, characters, shot) + results.push(areaShotRule) + + } return results } diff --git a/src/js/shot-explorer/index.js b/src/js/shot-explorer/index.js index ba4fa44bd1..2a13adc7a5 100644 --- a/src/js/shot-explorer/index.js +++ b/src/js/shot-explorer/index.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useLayoutEffect, useMemo } from 'react' +import React, { useState, useEffect, useLayoutEffect, useRef, useMemo } from 'react' import ShotMaker from './ShotMaker' import { Provider, connect, useSelector } from 'react-redux' import { Canvas } from 'react-three-fiber' @@ -6,9 +6,10 @@ import { useThree, useFrame } from 'react-three-fiber' import ShotExplorerSceneManager from './ShotExplorerSceneManager' import FatalErrorBoundary from '../shot-generator/components/FatalErrorBoundary' import {OutlineEffect} from '../vendor/OutlineEffect' -import {cache} from '../shot-generator/hooks/use-assets-manager' +import { getSceneObjects } from '../shared/reducers/shot-generator' import TWEEN from '@tweenjs/tween.js' import electron from 'electron' +import deepEqualSelector from '../utils/deepEqualSelector' import FilepathsContext from '../shot-generator/contexts/filepaths' const { @@ -32,16 +33,31 @@ const Effect = ({ shouldRender }) => { return null } +const sceneObjectSelector = (state) => { + const sceneObjects = getSceneObjects(state) + + let newSceneObjects = {} + let keys = Object.keys(sceneObjects) + for(let i = 0; i < keys.length; i++) { + let key = keys[i] + if(sceneObjects[key].type !== "camera") + newSceneObjects[key] = sceneObjects[key] + } + return newSceneObjects +} + +const getSceneObjectsM = deepEqualSelector([sceneObjectSelector], (sceneObjects) => sceneObjects) + const ShotExplorer = React.memo(({ withState, aspectRatio, store, elementKey, canvasHeight, - board + board, + sceneObjects }) => { const [sceneInfo, setSceneInfo] = useState(null) - const [newAssetsLoaded, setLoadedAssets] = useState() const [shouldRender, setShouldRender] = useState(false) const storyboarderFilePath = useSelector(state => state.meta.storyboarderFilePath) @@ -53,6 +69,8 @@ const ShotExplorer = React.memo(({ setSceneInfo({camera, scene, gl}) } + const updateAssets = () => {setLoadedAssets({})} + const [windowWidth, setWindowWidth] = useState(window.innerWidth) const handleResize = () => { setWindowWidth(window.innerWidth) @@ -68,15 +86,11 @@ const ShotExplorer = React.memo(({ } }, []) - const updateAssets = () => {setLoadedAssets({})} - useEffect(() => { - cache.subscribe(updateAssets) window.addEventListener("beforeunload", stopUnload) electron.remote.getCurrentWindow().on("blur", hide) electron.remote.getCurrentWindow().on("focus", show) return () => { - cache.unsubscribe(updateAssets) window.removeEventListener("beforeunload", stopUnload) electron.remote.getCurrentWindow().removeListener("blur", hide) electron.remote.getCurrentWindow().removeListener("focus", show) @@ -130,7 +144,6 @@ const ShotExplorer = React.memo(({ sceneInfo={ sceneInfo } withState={ withState } aspectRatio={ aspectRatio } - newAssetsLoaded={ newAssetsLoaded } canvasHeight={ canvasHeight } elementKey={ elementKey }/> } @@ -144,6 +157,7 @@ export default connect( mainViewCamera: state.mainViewCamera, aspectRatio: state.aspectRatio, board: state.board, + sceneObjects: getSceneObjectsM(state) }), { withState, diff --git a/src/js/shot-explorer/utils/isSuitableForIk.js b/src/js/shot-explorer/utils/isSuitableForIk.js new file mode 100644 index 0000000000..7953c4cbc3 --- /dev/null +++ b/src/js/shot-explorer/utils/isSuitableForIk.js @@ -0,0 +1,26 @@ + +let ikBonesName = ["Hips", "Spine", "Spine1", "Spine2", "Neck", "Head", + "LeftShoulder", "LeftArm", "LeftForeArm", "LeftHand", + "RightShoulder", "RightArm", "RightForeArm", "RightHand", + "LeftUpLeg", "LeftLeg", "LeftFoot", + "RightUpLeg", "RightLeg", "RightFoot"] +const isSuitableForIk = (skeleton) => { + //let isSuitable = true + let foundBones = [] + for(let i = 0; i < skeleton.bones.length; i++) { + let bone = skeleton.bones[i] + let ikBoneName = ikBonesName.filter(name => bone.name.includes(name))[0] + if(ikBoneName) { + foundBones.push(ikBoneName) + let indexOf = ikBonesName.indexOf(ikBoneName) + ikBonesName.splice(indexOf, 1) + bone.name = ikBoneName + bone.userData.name = ikBoneName + } + } + let isSiutable = ikBonesName.length === 0 + ikBonesName = ikBonesName.concat(foundBones) + return isSiutable +} + +export default isSuitableForIk \ No newline at end of file diff --git a/src/js/shot-generator/utils/cameraUtils.js b/src/js/shot-generator/utils/cameraUtils.js index 3bb86f7e2c..404df512ab 100644 --- a/src/js/shot-generator/utils/cameraUtils.js +++ b/src/js/shot-generator/utils/cameraUtils.js @@ -168,7 +168,7 @@ const getShotInfo = ({ box.expandByObject(characters[i]) } - if (characters.length > 1) { +/* if (characters.length > 1) { direction = new THREE.Vector3() for (let i = 0; i < characters.length - 1; i += 2) { @@ -179,7 +179,7 @@ const getShotInfo = ({ direction = camera.position.clone().sub(direction) direction.y = camera.y - } + } */ } else if (!ShotSizesInfo[shotSize]) { let center = new THREE.Vector3() box.getCenter(center) @@ -233,9 +233,10 @@ const getShotBox = (character, shotType = 0) => { /** If shot isn't provided, use eyes to calculate shot angle */ if (!ShotSizesInfo[shotType]) { let skinnedMesh = character.getObjectByProperty("type", "SkinnedMesh") - let bone = skinnedMesh.skeleton.bones.find((bone) => bone.name === 'leaf') - - let boneInfo = getBoneStartEndPos(bone) + let bone = skinnedMesh.skeleton.bones.find((bone) => bone.name === 'Head') + let headChild = bone.children[0] + if(!headChild) return box + let boneInfo = getBoneStartEndPos(headChild) box.expandByPoint(boneInfo.start) box.expandByPoint(boneInfo.end) @@ -283,7 +284,7 @@ const getClosestCharacter = (characters, camera) => { let dist = camera.position.distanceTo(target.position) let angle = charDir.clone().dot(cameraDir) - let frustum = new THREE.Frustum().setFromMatrix(new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse)) + let frustum = new THREE.Frustum().setFromProjectionMatrix(new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse)) let visible = frustum.containsPoint(target.position) if ((angle >= 1.0 || angle <= 0.0) && dist <= resultDistance && visible) { @@ -328,13 +329,14 @@ const setShot = ({ direction.applyQuaternion(quaternion) direction.setLength(currentDistance) - + clampedInfo.position.copy(clampedInfo.target).sub(direction) } if (clampedInfo.position.y < 0) { - clampedInfo.position.sub(direction.clone().setY(0).setLength(clampedInfo.position.y)) + let currentDistance = clampedInfo.position.distanceTo(clampedInfo.target) clampedInfo.position.y = 0 + clampedInfo.position.z = clampedInfo.target.z + currentDistance } camera.position.copy(clampedInfo.position) @@ -356,5 +358,6 @@ const setShot = ({ export { ShotSizes, ShotAngles, - setShot + setShot, + getClosestCharacter } diff --git a/src/js/windows/shot-explorer/window.js b/src/js/windows/shot-explorer/window.js index bd2ed23a3a..908274e45a 100644 --- a/src/js/windows/shot-explorer/window.js +++ b/src/js/windows/shot-explorer/window.js @@ -2,7 +2,6 @@ const ReactDOM = require('react-dom') const React = require('react') const { ipcRenderer, shell } = electron = require('electron') const { Provider, batch } = require('react-redux') -const { dialog } = electron.remote const THREE = require('three') const { createStore, applyMiddleware, compose } = require('redux') const thunkMiddleware = require('redux-thunk').default @@ -20,15 +19,16 @@ const { loadScene, resetScene, } = require('../../shared/reducers/shot-generator') +const {batchActions, enableBatching} = require('redux-batched-actions') const i18n = require('../../services/i18next.config') require("../../shared/helpers/monkeyPatchGrayscale") + let sendedAction = [] let isBoardShown = false let isBoardLoaded = false let componentKey = THREE.Math.generateUUID() let shotExplorerElement -let isVisible = electron.remote.getCurrentWindow().visible let defaultHeight = 800 let canvasHeight = 400 let minimumWidth = 300 @@ -51,11 +51,11 @@ const composeEnhancers = ( const configureStore = function configureStore (preloadedState) { const store = createStore( - reducer, + enableBatching(reducer), preloadedState, composeEnhancers( applyMiddleware( - thunkMiddleware, store => next => action => { + thunkMiddleware, store => next => action => { let indexOf = sendedAction.indexOf(action) if(action && indexOf === -1) { ipcRenderer.send("shot-generator:updateStore", action) @@ -98,7 +98,6 @@ const showShotExplorer = () => { }, 100) return } - isVisible = true; pushUpdates(); isBoardShown = true; } @@ -153,25 +152,18 @@ electron.remote.getCurrentWindow().webContents.on('will-prevent-unload', event = isBoardShown = false }) -electron.remote.getCurrentWindow().on("hide", () => { - isVisible = false -}) - const pushUpdates = () => { shotExplorerElement = renderShotExplorer() - batch(() => { - for(let i = 0, length = sendedAction.length; i < length; i++) { - let object = sendedAction[i] - let action = object - if(!action.type) { - action = JSON.parse(object) - sendedAction.push(action) - } - store.dispatch(action) + for(let i = 0, length = sendedAction.length; i < length; i++) { + let object = sendedAction[i] + let action = object + if(!action.type) { + action = JSON.parse(object) + sendedAction[i] = action } - }) + } + store.dispatch(batchActions(sendedAction)) sendedAction = [] - renderDom() } electron.remote.getCurrentWindow().on("focus", () => {