toolsTabComponent.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import * as React from "react";
  2. import { PaneComponent, IPaneComponentProps } from "../paneComponent";
  3. import { LineContainerComponent } from "../lineContainerComponent";
  4. import { ButtonLineComponent } from "../lines/buttonLineComponent";
  5. import { Node } from "babylonjs/node";
  6. import { Nullable } from "babylonjs/types";
  7. import { VideoRecorder } from "babylonjs/Misc/videoRecorder";
  8. import { Tools } from "babylonjs/Misc/tools";
  9. import { EnvironmentTextureTools } from "babylonjs/Misc/environmentTextureTools";
  10. import { BackgroundMaterial } from "babylonjs/Materials/Background/backgroundMaterial";
  11. import { StandardMaterial } from "babylonjs/Materials/standardMaterial";
  12. import { PBRMaterial } from "babylonjs/Materials/PBR/pbrMaterial";
  13. import { CubeTexture } from "babylonjs/Materials/Textures/cubeTexture";
  14. import { Texture } from "babylonjs/Materials/Textures/texture";
  15. import { SceneSerializer } from "babylonjs/Misc/sceneSerializer";
  16. import { Mesh } from "babylonjs/Meshes/mesh";
  17. import { FilesInput } from 'babylonjs/Misc/filesInput';
  18. import { Scene } from 'babylonjs/scene';
  19. import { SceneLoaderAnimationGroupLoadingMode } from 'babylonjs/Loading/sceneLoader';
  20. import { GLTFComponent } from "./tools/gltfComponent";
  21. import { GLTFData, GLTF2Export } from "babylonjs-serializers/glTF/2.0/index";
  22. import { FloatLineComponent } from '../lines/floatLineComponent';
  23. import { IScreenshotSize } from 'babylonjs/Misc/interfaces/screenshotSize';
  24. import { NumericInputComponent } from '../lines/numericInputComponent';
  25. import { CheckBoxLineComponent } from '../lines/checkBoxLineComponent';
  26. import { TextLineComponent } from '../lines/textLineComponent';
  27. import { FileMultipleButtonLineComponent } from '../lines/fileMultipleButtonLineComponent';
  28. import { OptionsLineComponent } from '../lines/optionsLineComponent';
  29. import { MessageLineComponent } from '../lines/messageLineComponent';
  30. import { FileButtonLineComponent } from '../lines/fileButtonLineComponent';
  31. import { IndentedTextLineComponent } from '../lines/indentedTextLineComponent';
  32. const GIF = require('gif.js.optimized')
  33. export class ToolsTabComponent extends PaneComponent {
  34. private _videoRecorder: Nullable<VideoRecorder>;
  35. private _screenShotSize: IScreenshotSize = { precision: 1 };
  36. private _gifOptions = {width: 512, frequency: 200};
  37. private _useWidthHeight = false;
  38. private _isExporting = false;
  39. private _gifWorkerBlob: Blob;
  40. private _gifRecorder: any;
  41. private _previousRenderingScale: number;
  42. private _crunchingGIF = false;
  43. constructor(props: IPaneComponentProps) {
  44. super(props);
  45. this.state = { tag: "Record video" };
  46. const sceneImportDefaults = this.props.globalState.sceneImportDefaults;
  47. if (sceneImportDefaults["overwriteAnimations"] === undefined) {
  48. sceneImportDefaults["overwriteAnimations"] = true;
  49. }
  50. if (sceneImportDefaults["animationGroupLoadingMode"] === undefined) {
  51. sceneImportDefaults["animationGroupLoadingMode"] = SceneLoaderAnimationGroupLoadingMode.Clean;
  52. }
  53. }
  54. componentDidMount() {
  55. if (!(BABYLON as any).GLTF2Export) {
  56. Tools.LoadScript("https://preview.babylonjs.com/serializers/babylonjs.serializers.min.js", () => {
  57. });
  58. return;
  59. }
  60. }
  61. componentWillUnmount() {
  62. if (this._videoRecorder) {
  63. this._videoRecorder.stopRecording();
  64. this._videoRecorder.dispose();
  65. this._videoRecorder = null;
  66. }
  67. if (this._gifRecorder) {
  68. this._gifRecorder.render();
  69. this._gifRecorder = null;
  70. return;
  71. }
  72. }
  73. captureScreenshot() {
  74. const scene = this.props.scene;
  75. if (scene.activeCamera) {
  76. Tools.CreateScreenshot(scene.getEngine(), scene.activeCamera, this._screenShotSize);
  77. }
  78. }
  79. captureRender() {
  80. const scene = this.props.scene;
  81. const oldScreenshotSize: IScreenshotSize = {
  82. height: this._screenShotSize.height,
  83. width: this._screenShotSize.width,
  84. precision: this._screenShotSize.precision
  85. };
  86. if (!this._useWidthHeight) {
  87. this._screenShotSize.width = undefined;
  88. this._screenShotSize.height = undefined;
  89. }
  90. if (scene.activeCamera) {
  91. Tools.CreateScreenshotUsingRenderTarget(scene.getEngine(), scene.activeCamera, this._screenShotSize);
  92. }
  93. this._screenShotSize = oldScreenshotSize;
  94. }
  95. recordVideo() {
  96. if (this._videoRecorder && this._videoRecorder.isRecording) {
  97. this._videoRecorder.stopRecording();
  98. return;
  99. }
  100. const scene = this.props.scene;
  101. if (!this._videoRecorder) {
  102. this._videoRecorder = new VideoRecorder(scene.getEngine());
  103. }
  104. this._videoRecorder.startRecording().then(() => {
  105. this.setState({ tag: "Record video" });
  106. });
  107. this.setState({ tag: "Stop recording" });
  108. }
  109. recordGIFInternal() {
  110. const workerUrl = URL.createObjectURL(this._gifWorkerBlob);
  111. this._gifRecorder = new GIF({
  112. workers: 2,
  113. quality: 10,
  114. workerScript: workerUrl
  115. });
  116. const scene = this.props.scene;
  117. const engine = scene.getEngine();
  118. this._previousRenderingScale = engine.getHardwareScalingLevel();
  119. engine.setHardwareScalingLevel(engine.getRenderWidth() / this._gifOptions.width | 0);
  120. let intervalId = setInterval(() => {
  121. if (!this._gifRecorder) {
  122. clearInterval(intervalId);
  123. return;
  124. }
  125. this._gifRecorder.addFrame(engine.getRenderingCanvas(), {delay: this._gifOptions.frequency});
  126. }, this._gifOptions.frequency);
  127. this._gifRecorder.on('finished', (blob: Blob) =>{
  128. this._crunchingGIF = false;
  129. Tools.Download(blob, "record.gif");
  130. this.forceUpdate();
  131. URL.revokeObjectURL(workerUrl);
  132. engine.setHardwareScalingLevel(this._previousRenderingScale);
  133. });
  134. this.forceUpdate();
  135. }
  136. recordGIF() {
  137. if (this._gifRecorder) {
  138. this._crunchingGIF = true;
  139. this.forceUpdate();
  140. this._gifRecorder.render();
  141. this._gifRecorder = null;
  142. return;
  143. }
  144. if (this._gifWorkerBlob) {
  145. this.recordGIFInternal();
  146. return;
  147. }
  148. Tools.LoadFileAsync("https://cdn.jsdelivr.net/gh//terikon/gif.js.optimized@0.1.6/dist/gif.worker.js").then(value => {
  149. this._gifWorkerBlob = new Blob([value], {
  150. type: 'application/javascript'
  151. });
  152. this.recordGIFInternal();
  153. });
  154. }
  155. importAnimations(event: any) {
  156. const scene = this.props.scene;
  157. const overwriteAnimations = this.props.globalState.sceneImportDefaults["overwriteAnimations"];
  158. const animationGroupLoadingMode = this.props.globalState.sceneImportDefaults["animationGroupLoadingMode"];
  159. var reload = function (sceneFile: File) {
  160. // If a scene file has been provided
  161. if (sceneFile) {
  162. var onSuccess = function (scene: Scene) {
  163. if (scene.animationGroups.length > 0) {
  164. let currentGroup = scene.animationGroups[0];
  165. currentGroup.play(true);
  166. }
  167. };
  168. (BABYLON as any).SceneLoader.ImportAnimationsAsync("file:", sceneFile, scene, overwriteAnimations, animationGroupLoadingMode, null, onSuccess);
  169. }
  170. };
  171. let filesInputAnimation = new FilesInput(scene.getEngine() as any, scene as any, () => { }, () => { }, () => { }, (remaining: number) => { }, () => { }, reload, () => { });
  172. filesInputAnimation.loadFiles(event);
  173. }
  174. shouldExport(node: Node): boolean {
  175. // No skybox
  176. if (node instanceof Mesh) {
  177. if (node.material) {
  178. const material = node.material as PBRMaterial | StandardMaterial | BackgroundMaterial;
  179. const reflectionTexture = material.reflectionTexture;
  180. if (reflectionTexture && reflectionTexture.coordinatesMode === Texture.SKYBOX_MODE) {
  181. return false;
  182. }
  183. }
  184. }
  185. return true;
  186. }
  187. exportGLTF() {
  188. const scene = this.props.scene;
  189. this._isExporting = true;
  190. this.forceUpdate();
  191. GLTF2Export.GLBAsync(scene, "scene", {
  192. shouldExportNode: (node) => this.shouldExport(node)
  193. }).then((glb: GLTFData) => {
  194. glb.downloadFiles();
  195. this._isExporting = false;
  196. this.forceUpdate();
  197. }).catch(reason => {
  198. this._isExporting = false;
  199. this.forceUpdate();
  200. });
  201. }
  202. exportBabylon() {
  203. const scene = this.props.scene;
  204. var strScene = JSON.stringify(SceneSerializer.Serialize(scene));
  205. var blob = new Blob([strScene], { type: "octet/stream" });
  206. Tools.Download(blob, "scene.babylon");
  207. }
  208. createEnvTexture() {
  209. const scene = this.props.scene;
  210. EnvironmentTextureTools.CreateEnvTextureAsync(scene.environmentTexture as CubeTexture)
  211. .then((buffer: ArrayBuffer) => {
  212. var blob = new Blob([buffer], { type: "octet/stream" });
  213. Tools.Download(blob, "environment.env");
  214. })
  215. .catch((error: any) => {
  216. console.error(error);
  217. alert(error);
  218. });
  219. }
  220. exportReplay() {
  221. this.props.globalState.recorder.export();
  222. this.forceUpdate();
  223. }
  224. startRecording() {
  225. this.props.globalState.recorder.trackScene(this.props.scene);
  226. this.forceUpdate();
  227. }
  228. applyDelta(file: File) {
  229. Tools.ReadFile(file, (data) => {
  230. this.props.globalState.recorder.applyDelta(data, this.props.scene);
  231. this.forceUpdate();
  232. });
  233. }
  234. render() {
  235. const scene = this.props.scene;
  236. if (!scene) {
  237. return null;
  238. }
  239. const sceneImportDefaults = this.props.globalState.sceneImportDefaults;
  240. var animationGroupLoadingModes = [
  241. { label: "Clean", value: SceneLoaderAnimationGroupLoadingMode.Clean },
  242. { label: "Stop", value: SceneLoaderAnimationGroupLoadingMode.Stop },
  243. { label: "Sync", value: SceneLoaderAnimationGroupLoadingMode.Sync },
  244. { label: "NoSync", value: SceneLoaderAnimationGroupLoadingMode.NoSync },
  245. ];
  246. return (
  247. <div className="pane">
  248. <LineContainerComponent globalState={this.props.globalState} title="CAPTURE">
  249. <ButtonLineComponent label="Screenshot" onClick={() => this.captureScreenshot()} />
  250. <ButtonLineComponent label={this.state.tag} onClick={() => this.recordVideo()} />
  251. </LineContainerComponent>
  252. <LineContainerComponent globalState={this.props.globalState} title="CAPTURE WITH RTT">
  253. <ButtonLineComponent label="Capture" onClick={() => this.captureRender()} />
  254. <div className="vector3Line">
  255. <FloatLineComponent label="Precision" target={this._screenShotSize} propertyName='precision' onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
  256. <CheckBoxLineComponent label="Use Width/Height" onSelect={ value => {
  257. this._useWidthHeight = value;
  258. this.forceUpdate();
  259. }} isSelected={() => this._useWidthHeight} />
  260. {
  261. this._useWidthHeight &&
  262. <div className="secondLine">
  263. <NumericInputComponent label="Width" precision={0} step={1} value={this._screenShotSize.width ? this._screenShotSize.width : 512} onChange={value => this._screenShotSize.width = value} />
  264. <NumericInputComponent label="Height" precision={0} step={1} value={this._screenShotSize.height ? this._screenShotSize.height : 512} onChange={value => this._screenShotSize.height = value} />
  265. </div>
  266. }
  267. </div>
  268. </LineContainerComponent>
  269. <LineContainerComponent globalState={this.props.globalState} title="GIF">
  270. {
  271. this._crunchingGIF &&
  272. <MessageLineComponent text="Creating the GIF file..." />
  273. }
  274. {
  275. !this._crunchingGIF &&
  276. <ButtonLineComponent label={this._gifRecorder ? "Stop" : "Record"} onClick={() => this.recordGIF()} />
  277. }
  278. {
  279. !this._crunchingGIF && !this._gifRecorder &&
  280. <>
  281. <FloatLineComponent label="Resolution" isInteger={true} target={this._gifOptions} propertyName="width" />
  282. <FloatLineComponent label="Frequency (ms)" isInteger={true} target={this._gifOptions} propertyName="frequency" />
  283. </>
  284. }
  285. </LineContainerComponent>
  286. <LineContainerComponent globalState={this.props.globalState} title="REPLAY">
  287. {
  288. !this.props.globalState.recorder.isRecording &&
  289. <ButtonLineComponent label="Start recording" onClick={() => this.startRecording()} />
  290. }
  291. {
  292. this.props.globalState.recorder.isRecording &&
  293. <IndentedTextLineComponent value={"Record in progress"}/>
  294. }
  295. {
  296. this.props.globalState.recorder.isRecording &&
  297. <ButtonLineComponent label="Generate delta file" onClick={() => this.exportReplay()} />
  298. }
  299. <FileButtonLineComponent label={`Apply delta file`} onClick={(file) => this.applyDelta(file)} accept=".json" />
  300. </LineContainerComponent>
  301. <LineContainerComponent globalState={this.props.globalState} title="SCENE IMPORT">
  302. <FileMultipleButtonLineComponent label="Import animations" accept="gltf" onClick={(evt: any) => this.importAnimations(evt)} />
  303. <CheckBoxLineComponent label="Overwrite animations" target={sceneImportDefaults} propertyName="overwriteAnimations" onSelect={value => {
  304. sceneImportDefaults["overwriteAnimations"] = value;
  305. this.forceUpdate();
  306. }} />
  307. {
  308. sceneImportDefaults["overwriteAnimations"] === false &&
  309. <OptionsLineComponent label="Animation merge mode" options={animationGroupLoadingModes} target={sceneImportDefaults} propertyName="animationGroupLoadingMode" />
  310. }
  311. </LineContainerComponent>
  312. <LineContainerComponent globalState={this.props.globalState} title="SCENE EXPORT">
  313. {
  314. this._isExporting &&
  315. <TextLineComponent label="Please wait..exporting" ignoreValue={true} />
  316. }
  317. {
  318. !this._isExporting &&
  319. <>
  320. <ButtonLineComponent label="Export to GLB" onClick={() => this.exportGLTF()} />
  321. <ButtonLineComponent label="Export to Babylon" onClick={() => this.exportBabylon()} />
  322. {
  323. !scene.getEngine().premultipliedAlpha && scene.environmentTexture && scene.environmentTexture._prefiltered && scene.activeCamera &&
  324. <ButtonLineComponent label="Generate .env texture" onClick={() => this.createEnvTexture()} />
  325. }
  326. </>
  327. }
  328. </LineContainerComponent>
  329. {
  330. (BABYLON as any).GLTFFileLoader &&
  331. <GLTFComponent scene={scene} globalState={this.props.globalState!} />
  332. }
  333. </div>
  334. );
  335. }
  336. }