- Home /
What is the proper way to draw previews for custom assets
I am generating a rotatable preview image for a custom asset. I’ve got it working pretty well, but I’m not sure the method I’m using is advisable. For now it is all consolidated into a single function that returns a Texture2D, and takes as parameters, the asset itself, a Quaternion(orientation) and Vector2(output texture size). This function is then used by the object’s custom inspector’s OnPreviewGUI
function.
I’m generating the preview image for the asset, by creating some temporary objects in the scene, on a particular layer. Once of the objects is a camera (culled to that layer), a light(also culled to that layer), and an instance of the asset I want to preview, reference by the appropriate render components. A RenderTexture
is created and attached to the camera. Finally, the camera.render
function and texture2d .Readpixels
function are used to grab the resultant frame.
For now, when the preview generation function is done, the objects are destroyed.
While this works pretty well, and object and preview-render screen draw (almost) properly, I’ve hit a couple of issues.
-One issue is that I’m creating these temporary objects, in whatever happens to be the current scene. So, Lights in the current scene (with cull everything) shine on my preview object. -Also, it’s possible there might be objects in the scene using that particular preview culling layer, that could end up visible in the preview.
To work around this I tried out creating a new scene at runtime using EditorSceneManager.NewScene(NewSceneSetup.EmptyScene,NewSceneMode.Additive)
.
Then moving the camera, render object, and preview lights, to the new scene with SceneManager.MoveGameObjectToScene
(I’m not sure how to create those objects in the preview scene, initially.)
Surprisingly, even after putting all the preview objects in the new scene, THEN doing the camera.render call; the light from the current editor scene continued to shine on my preview object! Unfortunately, the new scene also makes unity’s window title text flicker occasionally, even though I never call SetCurrentScene.
Perhaps I’m on the right track and have some error/omission in the code, or perhaps I’m totally off track, and should be doing this differently.
Any suggestions on how to fix these few issues are appreciated.
Here is the code: I’m excluding the some of object setup stuff, and just leaving the meat. Let me know if you want to see more.
private Scene previewScene;
override public Texture2D GenerateSnapshot(Quaternion rotation, Vector2 size, bool hideBackFaces)
{
int layerToUse = 8;
if (size.x < 64) { size.x = 64; size.y = 64; }
RenderTexture rendTextr = new RenderTexture((int)size.x, (int)size.y, 16, RenderTextureFormat.ARGB32);
rendTextr.antiAliasing = 8;
rendTextr.Create();
RenderTexture currentActiveRT = RenderTexture.active;
// Set the supplied RenderTexture as the active one
RenderTexture.active = rendTextr;
//setup object to render
GameObject edgeRendObject = new GameObject();
edgeRendObject.layer = layerToUse;
// edgeRendObject gets setup here (omitted)
GameObject camObject = new GameObject();
Camera cam = camObject.AddComponent<Camera>();
// cam setup goes here (omitted)
cam.cameraType = CameraType.Game;//.SceneView;//.Preview;
cam.cullingMask = 1 << layerToUse;
GameObject light1 = new GameObject();
Light l1 = light1.AddComponent<Light>();
l1.cullingMask = 1 << layerToUse;
//l1 setup here (omitted)
Scene currentScene = SceneManager.GetActiveScene();
if (!previewScene.IsValid())
{
previewScene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Additive);
SceneManager.SetActiveScene(currentScene);
}
SceneManager.MoveGameObjectToScene(camObject, previewScene);
SceneManager.MoveGameObjectToScene(edgeRendObject, previewScene);
SceneManager.MoveGameObjectToScene(light1, previewScene);
RenderTexture.active = rendTextr;
cam.Render();
Texture2D outputTexture = new Texture2D((int)size.x, (int)size.y, TextureFormat.RGB24, false);
outputTexture.ReadPixels(new Rect(0, 0, size.x, size.y), 0, 0);
outputTexture.Apply();
RenderTexture.active = currentActiveRT;
Object.DestroyImmediate(camObject);
Object.DestroyImmediate(edgeRendObject);
Object.DestroyImmediate(light1);
DestroyImmediate(rendTextr);
SceneManager.SetActiveScene(currentScene);
EditorSceneManager.CloseScene(previewScene, true);
return outputTexture;
}
Answer by Bunny83 · May 27, 2016 at 09:28 PM
There are several things you might want to look at:
First don't create that temporary gameobjects with "new GameObject" it will mark the scene as dirty. Use EditorUtility.CreateGameObjectWithHideFlags for such objects and make sure you pass "HideFlags.HideAndDontSave" as parameter. That will create a gameobject without being saved to the scene and also makes it invisible in the scene. Just like the SceneView camera and lights
Next thing is you shouldn't create and destroy the objects all the time. Create them in OnEnable and destroy them in OnDisable.
Unity uses an internal method to setup the custom lighting for the preview (InternalEditorUtility.SetCustomLighting / RemoveCustomLighting()). You can try setting the cameras cameraType to CameraType.Preview. I've never implemented a perview so i'm not sure what this actually affects.
Most of the preview logic in the editor directly renders the desired objects with Graphics.DrawMesh. Unity uses an helper class called "PreviewRenderUtility" which you should be able to use yourself. It wraps some of the internal methods like "Handles.SetCameraOnlyDrawMesh" which seems to restrict the passed camera to only render DrawMesh calls and nothing else in the scene. It also provides you a camera and 3 lights which are destroyed when you call CleanUp.
I'm not sure how you use the Texture2D you create in your method, but make sure you destroy it as well or it will leak. Unity always renders previews directly into RenderTextures. You shouldn't mess around with "RenderTexture.active". Just set the targetTexture of your camera to your rendertexture. That will make your camera render to the texture instead.
A RenderTexture is also a Texture, so i'm not sure why you copy the content over into a Texture2D?
Great reply thanks bunny! Looks like your point one will eli$$anonymous$$ate any need to destroy the objects, since the user wont see them, cool! It also eli$$anonymous$$ate the need to create a whole separate scene.
I tried changing to preview type camera; no difference that I could see. The returned texture is used in a EditorGUI.DrawTextureTransparent(rect, previewImageTexture);
command. I suspect I copied it into the Texture2D, because I destroy the renderTexture before it goes out of scope.
I was pretty much just trying to follow the example for the RenderTexture. I though I had TRIED simply assigning it to the camera and it didn't work (I guess I messed something up). Do you think I should NOT destroy the RenderTexture in the function, and ins$$anonymous$$d return it as a Texture?
Good spot on the possible memory leak (especially considering you didn't even SEE the code, LOL), I WAS assigning it to a member variable, and simply overwriting it, not cleaning it up! Hmm, perhaps rather than RETURN the texture, I should assign it to a REF parameter. this way, I can make sure I clean it up, if it exists already, everytime.
Thanks again.
Update: Ran into a few stumbling block, and one wall I can't get past.
When I render using my custom renderer (make GL drawing calls myself), I can check the hide flags, and only draw stuff for CameraType.Preview cameras. For the (different custom object type) preview where a $$anonymous$$eshFilter and $$anonymous$$eshRenderer are used to do the drawing, the $$anonymous$$esh appears in the scene view. Even though it's object was created with CreateGameObjectsWithHideFlags, as recommend. This may also explain why lights in the scene continue to effect it. I suspect however, I'm not quite understanding, how these hide flags are supposed to work. Or am I NOT supposed to use a $$anonymous$$EshRenderer like this, and INSTEAD use that Draw$$anonymous$$esh fuction, you were talking about? (wasn't quite sure how to apply that point you made.)
previewRendererObject = EditorUtility.CreateGameObjectWithHideFlags("Preview Object", HideFlags.HideAndDontSave, new System.Type[] {typeof($$anonymous$$eshRenderer),typeof($$anonymous$$eshFilter)});
$$anonymous$$eshFilter meshFilter = previewRendererObject.GetComponent<$$anonymous$$eshFilter>();
if (meshFilter.shared$$anonymous$$esh == null) //sanity check
{
meshFilter.shared$$anonymous$$esh = Create$$anonymous$$eshProcedurally();
}
Hi @Bunny83, I'm so sorry, but I'm afraid I have to UNACCEPT this as an answer!
It took quite a while to implement all your suggestions, but alas:
1- using the HideFlags on a camera, does not prevent other scene objects from showing up in the camera (if they happen to be on the "preview layer"). (Does work: Drawing $$anonymous$$eshes with the PreviewRenderUtility, DOES show only that item in the camera.)
2- using HideFlags on preview objects, AND/OR using PreviewenderUility does not prevent the previewed objects from being affected by existing lights in the scene.
Though you do have good suggestions in there, unfortunately none of them seem to fix the original issue. If you have any other thoughts on whats going on here, and why it's not working you expected for me, I'd love to hear them before I deselect this as an accepted answer,
Edit/additional info: I tried to keep my PreviewRenderUtility code similar to the unity code I found here: https://github.com/$$anonymous$$attRix/UnityDecompiled/blob/cc432a3de42b53920d5d5dae85968ff993f4ec0e/UnityEditor/UnityEditor/$$anonymous$$odelInspector.cs
Alas I think it's this (damned) Internal function that does the lights for rendering previews, (the one you mentioned): InternalEditorUtility.SetCustomLighting(previewUtility.m_Light, ambient);
Unfortunately, it appears, this functionality is NOT wrapped up in the PreviewRednerUtility Class for us. So, I have no idea how to actually access it for use. I also have no idea what it actually does in there, so I don't know how to re-invent it.
Of course hideflags do not influence what objects are drawn. They only control the serialization system and the visibility in the inspector / hierarchy. Unity has a lot built-in features which we can't really recreated with the given API. That includes the custom lighting setup. It most likely directly influences the native rendering engine and temporarily ignores everything else until "RemoveCustomLighting" is called.
To recreate this functionality you would have to manually find all lights in the scene, disable them and re-enable them once finished rendering. This is of course quite bad for performance. If you want a reliable method you might want to use reflection to call "SetCustomLighting" and "RemoveCustomLighting". If the reflection can't find the method you might want to fallback to the manual solution. I don't really see another way. The ability to draw custom previews seems not too well designed by Unity. It's not really an often used feature...
I don't think you'll get a better answer any time soon ^^. I've digged quite deep through Unity's internals over the past years and haven't found anything that could help here.
"Of course.." oops, misunderstood.
Regarding the disable then re-enable of scene lights: Yeah, and I'd need to do the same for all visible objects, or they might show up in the background (for my objects rendered with GL draw functions). I guess I'll just have to cheat this: put preview objects and camera far from origin, and set a short far-clipping plane on the camera.
If there is no "proper-way" to do previews proscribed/provided by unity, perhaps we can do this from outside "the box"?
For the lights: I guess I can create custom "preview" shader, and assign that to the preview objects. The shader can use a material property to light the object, rather than the light provided by the shader rendering-pipeline. I can then configure the preview lights though the material.
Hmmm, I guess I should also look into "replacement shaders" for the preview camera (never used 'em before). Perhaps that can help me get around the "scene objects in the preview background" issue. I'm hoping for something will force the camera-shader to only draw "preview textures".
Yeesh, what a can o' worms!
If I'm able to come up with a better solution using shaders, I'll post that and change the accepted ans to that. Otherwise, I think your right about not getting a better answer: $$anonymous$$ANY thanks.
Answer by Glurth · Jun 01, 2016 at 03:47 PM
Here is what I finally came up with, after much assistance from Bunny83.
There are two types of objects I would like to preview, some that are made of meshes, some that are drawn with GL functions.
The GL function type turned out to be far simpler than I initially thought, since this code does not need an actual camera to render! Simply setting RenterTexture.active, then calling the drawing functions, draws on the activeRenderTexture. In this case, there is no need to call camera.Render(), which obviously prevents other objects in the scene from being visible (though I do I call GL.Clear()
, just before drawing the preview, just in-case). Even though we don’t use it to render, this DOES require that the camera be passed to the drawing function, so that the appropriate projection and view Matrixes can be specified in GL.
Getting the meshes to appear alone, and ignoring scene lighting, both involved creating a custom previewShader. The shader uses a couple of parameters to define the lighting, rather than the normal rendering pipline. This shader’s lighting can be used for both the GL drawing, as well as meshes. This was all pretty standard shader stuff, once I thought of it, except for one additional factor: The shader also defined a particular tag { “PreviewTag” = “PreviewTag” }
.
To render the mesh previews, rather than use Camera.Render()
, I used Camera.RenderWithShader(previewShader,” PreviewTag”)
. What this does, is effectively, only draw objects that use the previewShader (or any shader with the “PreviewTag” set to “PreviewTag”). If we assume that only the preview objects use that previewShader, we can also assume it’s the only object rendered by the camera. (I figured that if user-created objects assigned a shader called "Preview Shader", showed up in the previews; that is acceptable.)
As Bunny recommended in his answer, using the HideFlags, and the EditorUtility.CreateGameObjectWithHideFlags
function to create the preview objects (and preview camera) is the best way to keep them from showing up in the hierarchy.
Your answer
Follow this Question
Related Questions
Object shown on top of another, rendering priorities? 2 Answers
Any idea how to use PreviewRenderUtility ? 3 Answers
How to get Project view's preview icon size? 0 Answers
Get default preview for GameObject Editor 2 Answers
Changing the position of the light in the Animation Preview window 3 Answers