renderingZone.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import * as React from "react";
  2. import { GlobalState } from "../globalState";
  3. import { Engine } from "babylonjs/Engines/engine";
  4. import { SceneLoader } from "babylonjs/Loading/sceneLoader";
  5. import { GLTFFileLoader } from "babylonjs-loaders/glTF/glTFFileLoader";
  6. import { Scene } from "babylonjs/scene";
  7. import { Vector3 } from "babylonjs/Maths/math.vector";
  8. import { ArcRotateCamera } from "babylonjs/Cameras/arcRotateCamera";
  9. import { FramingBehavior } from "babylonjs/Behaviors/Cameras/framingBehavior";
  10. import { EnvironmentTools } from "../tools/environmentTools";
  11. import { Tools } from "babylonjs/Misc/tools";
  12. import { FilesInput } from "babylonjs/Misc/filesInput";
  13. import { Animation } from "babylonjs/Animations/animation";
  14. import { PBRBaseMaterial, PBRMaterial, StringTools, Texture } from "babylonjs";
  15. import { Mesh } from "babylonjs/Meshes/mesh";
  16. require("../scss/renderingZone.scss");
  17. function isTextureAsset(name: string): boolean {
  18. var queryStringIndex = name.indexOf("?");
  19. if (queryStringIndex !== -1) {
  20. name = name.substring(0, queryStringIndex);
  21. }
  22. return (
  23. StringTools.EndsWith(name, ".ktx") ||
  24. StringTools.EndsWith(name, ".ktx2") ||
  25. StringTools.EndsWith(name, ".png") ||
  26. StringTools.EndsWith(name, ".jpg") ||
  27. StringTools.EndsWith(name, ".jpeg")
  28. );
  29. }
  30. interface IRenderingZoneProps {
  31. globalState: GlobalState;
  32. assetUrl?: string;
  33. autoRotate?: boolean;
  34. cameraPosition?: Vector3;
  35. expanded: boolean;
  36. }
  37. export class RenderingZone extends React.Component<IRenderingZoneProps> {
  38. private _currentPluginName?: string;
  39. private _engine: Engine;
  40. private _scene: Scene;
  41. private _canvas: HTMLCanvasElement;
  42. public constructor(props: IRenderingZoneProps) {
  43. super(props);
  44. }
  45. initEngine() {
  46. this._canvas = document.getElementById("renderCanvas") as HTMLCanvasElement;
  47. this._engine = new Engine(this._canvas, true, { premultipliedAlpha: false, preserveDrawingBuffer: true });
  48. this._engine.loadingUIBackgroundColor = "#2A2342";
  49. // Resize
  50. window.addEventListener("resize", () => {
  51. this._engine.resize();
  52. });
  53. this.loadAsset();
  54. // File inputs
  55. let filesInput = new FilesInput(
  56. this._engine,
  57. null,
  58. (sceneFile: File, scene: Scene) => {
  59. this._scene = scene;
  60. this.onSceneLoaded(sceneFile.name);
  61. },
  62. null,
  63. null,
  64. null,
  65. () => {
  66. Tools.ClearLogCache();
  67. if (this._scene) {
  68. this.props.globalState.isDebugLayerEnabled = this.props.globalState.currentScene.debugLayer.isVisible();
  69. if (this.props.globalState.isDebugLayerEnabled) {
  70. this._scene.debugLayer.hide();
  71. }
  72. }
  73. },
  74. null,
  75. (file, scene, message) => {
  76. this.props.globalState.onError.notifyObservers({ message: message });
  77. }
  78. );
  79. filesInput.onProcessFileCallback = (file, name, extension, setSceneFileToLoad) => {
  80. if (filesInput.filesToLoad && filesInput.filesToLoad.length === 1 && extension) {
  81. switch (extension.toLowerCase()) {
  82. case "dds":
  83. case "env":
  84. case "hdr": {
  85. FilesInput.FilesToLoad[name] = file;
  86. EnvironmentTools.SkyboxPath = "file:" + (file as any).correctName;
  87. return false;
  88. }
  89. default: {
  90. if (isTextureAsset(name)) {
  91. setSceneFileToLoad(file);
  92. }
  93. break;
  94. }
  95. }
  96. }
  97. return true;
  98. };
  99. filesInput.loadAsync = (sceneFile, onProgress) => {
  100. const filesToLoad = filesInput.filesToLoad;
  101. if (filesToLoad.length === 1) {
  102. const fileName = (filesToLoad[0] as any).correctName;
  103. if (isTextureAsset(fileName)) {
  104. return Promise.resolve(this.loadTextureAsset(`file:${fileName}`));
  105. }
  106. }
  107. return SceneLoader.LoadAsync("file:", sceneFile, this._engine, onProgress);
  108. };
  109. filesInput.monitorElementForDragNDrop(this._canvas);
  110. this.props.globalState.filesInput = filesInput;
  111. window.addEventListener("keydown", (event) => {
  112. // Press R to reload
  113. if (event.keyCode === 82 && event.target && (event.target as HTMLElement).nodeName !== "INPUT" && this._scene) {
  114. if (this.props.assetUrl) {
  115. this.loadAssetFromUrl();
  116. } else {
  117. filesInput.reload();
  118. }
  119. }
  120. });
  121. }
  122. prepareCamera() {
  123. // Attach camera to canvas inputs
  124. if (!this._scene.activeCamera) {
  125. this._scene.createDefaultCamera(true);
  126. const camera = this._scene.activeCamera! as ArcRotateCamera;
  127. if (this._currentPluginName === "gltf") {
  128. // glTF assets use a +Z forward convention while the default camera faces +Z. Rotate the camera to look at the front of the asset.
  129. camera.alpha += Math.PI;
  130. }
  131. // Enable camera's behaviors
  132. camera.useFramingBehavior = true;
  133. var framingBehavior = camera.getBehaviorByName("Framing") as FramingBehavior;
  134. framingBehavior.framingTime = 0;
  135. framingBehavior.elevationReturnTime = -1;
  136. if (this._scene.meshes.length) {
  137. camera.lowerRadiusLimit = null;
  138. var worldExtends = this._scene.getWorldExtends(function (mesh) {
  139. return mesh.isVisible && mesh.isEnabled();
  140. });
  141. framingBehavior.zoomOnBoundingInfo(worldExtends.min, worldExtends.max);
  142. }
  143. if (this.props.autoRotate) {
  144. camera.useAutoRotationBehavior = true;
  145. }
  146. if (this.props.cameraPosition) {
  147. camera.setPosition(this.props.cameraPosition);
  148. }
  149. camera.pinchPrecision = 200 / camera.radius;
  150. camera.upperRadiusLimit = 5 * camera.radius;
  151. camera.wheelDeltaPercentage = 0.01;
  152. camera.pinchDeltaPercentage = 0.01;
  153. }
  154. this._scene.activeCamera!.attachControl();
  155. }
  156. handleErrors() {
  157. // In case of error during loading, meshes will be empty and clearColor is set to red
  158. if (this._scene.meshes.length === 0 && this._scene.clearColor.r === 1 && this._scene.clearColor.g === 0 && this._scene.clearColor.b === 0) {
  159. this._canvas.style.opacity = "0";
  160. this.props.globalState.onError.notifyObservers({ scene: this._scene, message: "No mesh found in your scene" });
  161. } else {
  162. if (Tools.errorsCount > 0) {
  163. this.props.globalState.onError.notifyObservers({ scene: this._scene, message: "Scene loaded but several errors were found" });
  164. }
  165. // this._canvas.style.opacity = "1";
  166. let camera = this._scene.activeCamera! as ArcRotateCamera;
  167. if (camera.keysUp) {
  168. camera.keysUp.push(90); // Z
  169. camera.keysUp.push(87); // W
  170. camera.keysDown.push(83); // S
  171. camera.keysLeft.push(65); // A
  172. camera.keysLeft.push(81); // Q
  173. camera.keysRight.push(69); // E
  174. camera.keysRight.push(68); // D
  175. }
  176. }
  177. }
  178. prepareLighting() {
  179. if (this._currentPluginName === "gltf") {
  180. if (!this._scene.environmentTexture) {
  181. this._scene.environmentTexture = EnvironmentTools.LoadSkyboxPathTexture(this._scene);
  182. }
  183. if (this._scene.environmentTexture) {
  184. this._scene.createDefaultSkybox(this._scene.environmentTexture, true, (this._scene.activeCamera!.maxZ - this._scene.activeCamera!.minZ) / 2, 0.3, false);
  185. }
  186. } else {
  187. var pbrPresent = false;
  188. for (const material of this._scene.materials) {
  189. if (material instanceof PBRBaseMaterial) {
  190. pbrPresent = true;
  191. break;
  192. }
  193. }
  194. if (pbrPresent) {
  195. if (!this._scene.environmentTexture) {
  196. this._scene.environmentTexture = EnvironmentTools.LoadSkyboxPathTexture(this._scene);
  197. }
  198. } else {
  199. this._scene.createDefaultLight();
  200. }
  201. }
  202. }
  203. onSceneLoaded(filename: string) {
  204. this._engine.clearInternalTexturesCache();
  205. this._scene.skipFrustumClipping = true;
  206. this.props.globalState.onSceneLoaded.notifyObservers({ scene: this._scene, filename: filename });
  207. this.prepareCamera();
  208. this.prepareLighting();
  209. this.handleErrors();
  210. if (this.props.globalState.isDebugLayerEnabled) {
  211. this.props.globalState.showDebugLayer();
  212. }
  213. delete this._currentPluginName;
  214. }
  215. loadTextureAsset(url: string): Scene {
  216. const scene = new Scene(this._engine);
  217. const plane = Mesh.CreatePlane("plane", 1, scene);
  218. const texture = new Texture(url, scene, undefined, undefined, Texture.NEAREST_LINEAR, () => {
  219. const size = texture.getBaseSize();
  220. if (size.width > size.height) {
  221. plane.scaling.y = size.height / size.width;
  222. } else {
  223. plane.scaling.x = size.width / size.height;
  224. }
  225. texture.gammaSpace = true;
  226. texture.hasAlpha = true;
  227. texture.wrapU = Texture.CLAMP_ADDRESSMODE;
  228. texture.wrapV = Texture.CLAMP_ADDRESSMODE;
  229. scene.debugLayer.show();
  230. scene.debugLayer.select(texture, "PREVIEW");
  231. }, (message, exception) => {
  232. this.props.globalState.onError.notifyObservers({ scene: scene, message: message || exception.message || "Failed to load texture" });
  233. });
  234. const material = new PBRMaterial("unlit", scene);
  235. material.unlit = true;
  236. material.albedoTexture = texture;
  237. material.alphaMode = PBRMaterial.PBRMATERIAL_ALPHABLEND;
  238. plane.material = material;
  239. return scene;
  240. }
  241. loadAssetFromUrl() {
  242. const assetUrl = this.props.assetUrl!;
  243. const rootUrl = Tools.GetFolderPath(assetUrl);
  244. const fileName = Tools.GetFilename(assetUrl);
  245. const promise = isTextureAsset(assetUrl)
  246. ? Promise.resolve(this.loadTextureAsset(assetUrl))
  247. : SceneLoader.LoadAsync(rootUrl, fileName, this._engine);
  248. promise
  249. .then((scene) => {
  250. if (this._scene) {
  251. this._scene.dispose();
  252. }
  253. this._scene = scene;
  254. this.onSceneLoaded(fileName);
  255. scene.whenReadyAsync().then(() => {
  256. this._engine.runRenderLoop(() => {
  257. scene.render();
  258. });
  259. });
  260. })
  261. .catch((reason) => {
  262. this.props.globalState.onError.notifyObservers({ message: reason.message });
  263. });
  264. }
  265. loadAsset() {
  266. if (this.props.assetUrl) {
  267. this.loadAssetFromUrl();
  268. return;
  269. }
  270. }
  271. componentDidMount() {
  272. if (!Engine.isSupported()) {
  273. return;
  274. }
  275. Engine.ShadersRepository = "/src/Shaders/";
  276. // This is really important to tell Babylon.js to use decomposeLerp and matrix interpolation
  277. Animation.AllowMatricesInterpolation = true;
  278. // Setting up some GLTF values
  279. GLTFFileLoader.IncrementalLoading = false;
  280. SceneLoader.OnPluginActivatedObservable.add((plugin) => {
  281. this._currentPluginName = plugin.name;
  282. if (this._currentPluginName === "gltf") {
  283. (plugin as GLTFFileLoader).onValidatedObservable.add((results) => {
  284. if (results.issues.numErrors > 0) {
  285. this.props.globalState.showDebugLayer();
  286. }
  287. });
  288. }
  289. });
  290. this.initEngine();
  291. }
  292. shouldComponentUpdate(nextProps: IRenderingZoneProps) {
  293. if (nextProps.expanded !== this.props.expanded) {
  294. setTimeout(() => this._engine.resize());
  295. return true;
  296. }
  297. return false;
  298. }
  299. public render() {
  300. return (
  301. <div id="canvasZone" className={this.props.expanded ? "expanded" : ""}>
  302. <canvas id="renderCanvas" touch-action="none" onContextMenu={(evt) => evt.preventDefault()}></canvas>
  303. </div>
  304. );
  305. }
  306. }