UNPKG

prendy

Version:

Make games with prerendered backdrops using babylonjs and repond

278 lines (277 loc) 12.2 kB
import { Camera, Matrix, Vector3 } from "@babylonjs/core"; import { shortenDecimals } from "chootils/dist/numbers"; import { copyPoint, defaultPosition } from "chootils/dist/points2d"; import { measurementToRect, pointInsideRect } from "chootils/dist/rects"; import { defaultSize } from "chootils/dist/sizes"; import { getRefs, getState, onNextTick } from "repond"; import { meta } from "../../meta"; import { getGlobalState, setGlobalState } from "../prendyUtils/global"; import { getEngine } from "./getSceneOrEngineUtils"; export function getScreenSize() { return { x: window.innerWidth, y: window.innerHeight }; } export const slateSize = { x: 2048, y: 2048 }; export function getProjectionMatrixCustomSize(theCamera, theSize) { // Only for perspective camera here :) const scene = theCamera.getScene(); // const aspectRatio = engine.getAspectRatio(theCamera); const aspectRatio = theSize.x / theSize.y; let theProjectionMatrix = Matrix.Identity(); if (theCamera.minZ <= 0) { theCamera.minZ = 0.1; } if (scene.useRightHandedSystem) { Matrix.PerspectiveFovRHToRef(theCamera.fov, aspectRatio, theCamera.minZ, theCamera.maxZ, theProjectionMatrix, theCamera.fovMode === Camera.FOVMODE_VERTICAL_FIXED); } else { Matrix.PerspectiveFovLHToRef(theCamera.fov, aspectRatio, theCamera.minZ, theCamera.maxZ, theProjectionMatrix, theCamera.fovMode === Camera.FOVMODE_VERTICAL_FIXED); } return theProjectionMatrix; } export function getPositionOnSlate(theMesh, modelName) { const { prendyOptions } = meta.assets; const globalRefs = getRefs().global.main; // This is a position on the slate itself const { nowPlaceName, nowCamName } = getState().global.main; const placeRefs = getRefs().places[nowPlaceName]; const nowCam = placeRefs.camsRefs[nowCamName]?.camera; if (!nowCam) return new Vector3(); // Use the characters head position instead of center position (for speech bubbles) const Y_OFFSET = prendyOptions.headHeightOffsets[modelName] ?? 2; // default to 2, just above the model return Vector3.Project(new Vector3(theMesh.position.x, theMesh.position.y + Y_OFFSET, theMesh.position.z), Matrix.Identity(), nowCam .getViewMatrix() // .multiply(currentCamera.getProjectionMatrix()), .multiply(getProjectionMatrixCustomSize(nowCam, slateSize)), nowCam.viewport.toGlobal(slateSize.x, slateSize.y)); } export function getSlatePositionNotOverEdges(slatePos, useGoal) { const globalRefs = getRefs().global.main; const newSlatePos = copyPoint(slatePos); const stretchVideoX = useGoal ? globalRefs.stretchVideoGoalSize.x : globalRefs.stretchVideoSize.x; const stretchVideoY = useGoal ? globalRefs.stretchVideoGoalSize.y : globalRefs.stretchVideoSize.y; const maxShiftX = (stretchVideoX - 1) / stretchVideoX / 2; const maxShiftY = (stretchVideoY - 1) / stretchVideoY / 2; const isOverRightEdge = newSlatePos.x > maxShiftX; const isOverLeftEdge = newSlatePos.x < -maxShiftX; const isOverBottomEdge = newSlatePos.y > maxShiftY; const isOverTopEdge = newSlatePos.y < -maxShiftY; const isOverEdge = isOverRightEdge || isOverLeftEdge || isOverBottomEdge || isOverTopEdge; if (isOverRightEdge) newSlatePos.x = maxShiftX; if (isOverLeftEdge) newSlatePos.x = -maxShiftX; if (isOverBottomEdge) newSlatePos.y = maxShiftY; if (isOverTopEdge) newSlatePos.y = -maxShiftY; newSlatePos.x = shortenDecimals(newSlatePos.x); newSlatePos.y = shortenDecimals(newSlatePos.y); // zoom 1.5, edges are 0.1625? // zoom 2, edges are 0.25 // zoom 2.5, edges are ~0.3? // zoom 3, edges are ~0.33? // zoom 1, edges are 0 // console.log("editing slate pos"); return newSlatePos; } function getMidNumber(a, b) { return a + (b - a) / 2; } function weightedInterpolation(a, b, weight) { return a * weight + b * (1 - weight); } function updateSlatePositionToFocusOnMesh({ meshRef, instant, model, }) { function updateSlatePos() { const characterPointOnSlate = getPositionOnSlate(meshRef, model); const { nowPlaceName, nowCamName } = getState().global.main; const camRefs = getRefs().places[nowPlaceName].camsRefs[nowCamName]; const defaultFocusPointOnSlateY = slateSize.y / 2; // focus on the middle of the screen, so widescreen can be composed nicely const focusPointOnSlateX = camRefs.focusPointOnSlate.x; const focusPointOnSlateY = camRefs.focusPointOnSlate.y ?? defaultFocusPointOnSlateY; const finalFocusPointOnSlate = { x: focusPointOnSlateX ? weightedInterpolation(focusPointOnSlateX, characterPointOnSlate.x, 0.5) : characterPointOnSlate.x, y: focusPointOnSlateY ? weightedInterpolation(focusPointOnSlateY, characterPointOnSlate.y, 0.5) : characterPointOnSlate.y, }; const newSlatePos = getSlatePositionNotOverEdges({ x: finalFocusPointOnSlate.x / slateSize.x - 0.5, y: 1 - finalFocusPointOnSlate.y / slateSize.y - 0.5, }); if (instant) { setGlobalState({ slatePosGoal: newSlatePos, slatePos: newSlatePos }); } else { setGlobalState({ slatePosGoal: newSlatePos }); } } if (instant) { updateSlatePos(); } else { onNextTick(updateSlatePos); } } export function focusSlateOnFocusedDoll(instant) { const { focusedDoll } = getState().global.main; const { meshRef } = getRefs().dolls[focusedDoll]; const model = getState().dolls[focusedDoll].modelName; if (!meshRef) return; updateSlatePositionToFocusOnMesh({ meshRef: meshRef, instant: !!instant, model }); } export function getViewSize() { const engine = getEngine(); if (!engine) return defaultSize(); return { width: engine.getRenderWidth(), height: engine.getRenderHeight(), }; } export function checkPointIsInsideSlate(pointOnSlate) { const globalRefs = getRefs().global.main; const sceneSize = slateSize; // 1920x1080 (the point is in here) const OUT_OF_FRAME_PADDING = 200; const pointSortOfIsInsideSlate = pointInsideRect(pointOnSlate, measurementToRect({ width: sceneSize.x + OUT_OF_FRAME_PADDING, height: sceneSize.y + OUT_OF_FRAME_PADDING * 3, // Y is easier to go over the edges when the camera angle's low x: 0 - OUT_OF_FRAME_PADDING, y: 0 - OUT_OF_FRAME_PADDING * 3, })); return pointSortOfIsInsideSlate; } // This includes after the slate moved export function convertPointOnSlateToPointOnScreen({ pointOnSlate, // point on slate goes from 0 - 1920, 0 - 1080, when the point is from the top left to bottom right slatePos, // slate position is 0 when centered, then its the amount of offset (in percentage?) slateZoom, // NOTE make sure to include the zoom multiplier }) { if (!slatePos) return defaultPosition(); const screenSize = getScreenSize(); const slatePosPixels = { x: slatePos.x * slateSize.x, y: slatePos.y * slateSize.y, }; const center = { x: slateSize.x / 2, y: slateSize.y / 2 }; // somehow this works function transformIt(point, scale, translation = { x: 0, y: 0 }) { const transformedPoint = { x: (point.x + translation.x - center.x) * scale + center.x, y: (point.y - translation.y - center.y) * scale + center.y, }; return transformedPoint; } const transformedPoint = transformIt(pointOnSlate, slateZoom, { x: -slatePosPixels.x, y: -slatePosPixels.y, }); const slateToScreenSize = { x: screenSize.x / slateSize.x, y: screenSize.y / slateSize.y, }; const slateRatio = slateSize.x / slateSize.y; // 16/9 const screenRatio = screenSize.x / screenSize.y; const screenIsThinnerThenVideo = screenRatio < slateRatio; let heightDifference = 0; let widthDifference = 0; let pixelScaler = 1; let slateRelativeScreenSize = { x: screenSize.x, y: screenSize.y, }; if (screenIsThinnerThenVideo) { slateRelativeScreenSize = { x: screenSize.x / slateToScreenSize.y, y: screenSize.y / slateToScreenSize.y, }; widthDifference = (slateRelativeScreenSize.x - slateSize.x) / 2; // scale based on fixed height pixelScaler = slateToScreenSize.y; } else { slateRelativeScreenSize = { x: screenSize.x / slateToScreenSize.x, y: screenSize.y / slateToScreenSize.x, }; heightDifference = (slateRelativeScreenSize.y - slateSize.y) / 2; // scale based on fixed width pixelScaler = slateToScreenSize.x; } // NOTE this is only working when it's thinner // IN THIN MODE, the height is consistant, // in WIDE mode the width is consistant let positionOnScreen = { x: (transformedPoint.x + widthDifference) * pixelScaler, y: (transformedPoint.y + heightDifference) * pixelScaler, }; return positionOnScreen; } export function getShaderTransformStuff() { const globalRefs = getRefs().global.main; const { slateZoom: slateZoomUnmultiplied, slateZoomGoal: slateZoomGoalUnmultiplied } = getState().global.main; const zoomMultiplier = getGlobalState().zoomMultiplier; const slateZoom = slateZoomUnmultiplied * zoomMultiplier; const slateZoomGoal = slateZoomGoalUnmultiplied * zoomMultiplier; // NOTE engine.getRenderHeight will return the 'retina'/upscaled resolution const screenWidth = window.innerWidth; const screenHeight = window.innerHeight; // check the screen ratio, and compare that to the video ratio const videoRatio = slateSize.x / slateSize.y; // 16/9 const screenRatio = screenWidth / screenHeight; const videoXDiff = slateSize.x / screenWidth; const videoYDiff = slateSize.y / screenHeight; const ratioDiff = screenRatio / videoRatio; let stretchVideoX = 1; let stretchVideoY = 1; let stretchVideoGoalX = 1; let stretchVideoGoalY = 1; const screenIsThinnerThenVideo = screenRatio < videoRatio; // Changing width means same babylon camera zoom, but changing height zooms out, // because of camera.fovMode = Camera.FOVMODE_VERTICAL_FIXED; // the stretch for each is 1 for full stretch const editedSlateZoomX = slateZoom / videoXDiff; const editedSlateZoomY = slateZoom / videoYDiff; const editedSlateZoomGoalX = slateZoomGoal / videoXDiff; const editedSlateZoomGoalY = slateZoomGoal / videoYDiff; let editedSlateSceneZoom = slateZoom; stretchVideoX = editedSlateZoomY * Math.abs(videoXDiff); stretchVideoY = editedSlateZoomY + (Math.abs(videoYDiff) - 1); if (screenIsThinnerThenVideo) { stretchVideoX = editedSlateZoomY * Math.abs(videoXDiff); stretchVideoY = slateZoom; stretchVideoGoalX = editedSlateZoomGoalY * Math.abs(videoXDiff); stretchVideoGoalY = slateZoomGoal; } else { stretchVideoX = slateZoom; stretchVideoY = editedSlateZoomX * Math.abs(videoYDiff); editedSlateSceneZoom = slateZoom * (screenRatio / videoRatio); stretchVideoGoalX = slateZoomGoal; stretchVideoGoalY = editedSlateZoomGoalX * Math.abs(videoYDiff); } let stretchSceneX = editedSlateSceneZoom / ratioDiff; let stretchSceneY = editedSlateSceneZoom; const editedHardwareScaling = 1 / editedSlateSceneZoom; globalRefs.stretchVideoGoalSize.x = stretchVideoGoalX; globalRefs.stretchVideoGoalSize.y = stretchVideoGoalY; globalRefs.stretchVideoSize.x = stretchVideoX; globalRefs.stretchVideoSize.y = stretchVideoY; globalRefs.stretchSceneSize.x = stretchSceneX; globalRefs.stretchSceneSize.y = stretchSceneY; return { editedSlateSceneZoom, editedHardwareScaling, }; } // { // getPositionOnSlate, // focusSlateOnFocusedDoll, // getSlatePositionNotOverEdges, // getViewSize, // convertPointOnSlateToPointOnScreen, // checkPointIsInsideSlate, // getShaderTransformStuff, // };