Skip to content

Commit

Permalink
feat: initial implementation of preview system API v0.3 (#254)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdunderscore authored Jun 7, 2024
1 parent b692d2f commit 83761b0
Show file tree
Hide file tree
Showing 14 changed files with 405 additions and 546 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Deprecated

## [1.5.0-alpha.1] - [2024-06-06]

### Changed (since alpha.0)

- Redid `IRenderFilter` API

## [1.5.0-alpha.0] - [2024-06-02]

### Added
Expand Down
185 changes: 76 additions & 109 deletions Editor/PreviewSystem/IRenderFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,154 +3,121 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using nadena.dev.ndmf.rq;
using UnityEngine;
using Object = UnityEngine.Object;

#endregion

namespace nadena.dev.ndmf.preview
{
/// <summary>
/// Represents the current state of a mesh. IRenderFilters mutate this state in order to perform heavyweight portions
/// of a preview rendering operation.
///
/// TODO: This API is likely to change radically in future alpha releases.
/// </summary>
public sealed class MeshState
public interface IRenderFilter
{
internal long NodeId { get; }
public ReactiveValue<IImmutableList<IImmutableList<Renderer>>> TargetGroups { get; }

/// <summary>
/// The original renderer that this MeshState was born from
/// Instantiates a node in the preview graph. This operation is used when creating a new proxy renderer, and may
/// perform relatively heavyweight operations to prepare the Mesh, Materials, and Textures for the renderer. It
/// may not modify other aspects of the renderer; however, these can be done in the OnFrame callback in the
/// returned IRenderFilterNode.
///
/// When making changes to meshes, textures, and materials, this node must create new instances of these objects,
/// and destroy them in `IRenderFilterNode.Dispose`.
/// </summary>
public Renderer Original { get; }

private bool _meshIsOwned;
private Mesh _mesh;
/// <param name="proxyPairs">An enumerable of (original, proxy) renderer pairs</param>
/// <param name="context">A compute context that is used to track which values your code depended on in
/// configuring this node. Changing these values will triger a recomputation of this node.</param>
/// <returns></returns>
public Task<IRenderFilterNode> Instantiate(IEnumerable<(Renderer, Renderer)> proxyPairs,
ComputeContext context);
}

public interface IRenderFilterNode : IDisposable
{
/// <summary>
/// The current mesh associated with this renderer. Important: When setting this value, you must set to a _new_
/// mesh. This new mesh will be destroyed when the preview state is recomputed.
/// The sharedMesh value
/// </summary>
public Mesh Mesh
{
get => _mesh;
set
{
_meshIsOwned = true;
_mesh = value;
if (_mesh != null) _mesh.name = "Mesh #" + NodeId;
}
}

private bool _materialsAreOwned;
private ImmutableList<Material> _materials;
public const ulong Mesh = 1;

/// <summary>
/// The materials associated with this mesh. Important: When setting this value, you must set to a list of entirely
/// _new_ materials. These new materials will be destroyed when the preview state is recomputed.
/// Materials and their properties
/// </summary>
public ImmutableList<Material> Materials
{
get => _materials;
set
{
_materials = value;
_materialsAreOwned = true;
}
}
public const ulong Material = 2;

/// <summary>
/// An event which will be invoked when the mesh state is discarded. This can be used to destroy any resources
/// you've created other than Meshes and Materials - e.g. textures.
/// The contents of the textures bound to materials
/// </summary>
public event Action OnDispose;

private bool _disposed = false;

internal MeshState(Renderer renderer)
{
Original = renderer;

if (renderer is SkinnedMeshRenderer smr)
{
Mesh = Object.Instantiate(smr.sharedMesh);
}
else if (renderer is MeshRenderer mr)
{
Mesh = Object.Instantiate(mr.GetComponent<MeshFilter>().sharedMesh);
}

Materials = renderer.sharedMaterials.Select(m => new Material(m)).ToImmutableList();
}

private MeshState(MeshState state, long nodeId)
{
Original = state.Original;
_mesh = state._mesh;
_materials = state._materials;
NodeId = nodeId;
}
public const ulong Texture = 4;

// Not IDisposable as we don't want to expose that as a public API
internal void Dispose()
{
if (_disposed) return;

if (_meshIsOwned) Object.DestroyImmediate(Mesh);
if (_materialsAreOwned)
{
foreach (var material in Materials)
{
Object.DestroyImmediate(material);
}
}
/// <summary>
/// Blendshapes and the bones array
/// </summary>
public const ulong Shapes = 8;

OnDispose?.Invoke();
}
public const ulong Everything = Mesh | Material | Texture | Shapes;

internal MeshState Clone(long nodeId)
{
return new MeshState(this, nodeId);
}
}
/// <summary>
/// Indicates which static aspects of a renderer this node examines. Changes to these aspects will trigger a
/// rebuild or partial update of this node.
/// </summary>
public ulong Reads { get; }

/// <summary>
/// An interface implemented by components which need to modify the appearance of a renderer for preview purposes.
/// </summary>
public interface IRenderFilter
{
/// <summary>
/// A list of lists of renderers this filter operates on. The outer list is a list of renderer groups; each
/// group of renderers will be passed to this filter as one unit, allowing for cross-renderer operation such
/// as texture atlasing.
/// Indicates which aspects of a renderer this node changed, relative to the node prior to the last Update
/// call. This may trigger updates of downstream nodes.
///
/// This value is ignored on the first generation of the node, created from `IRenderFilter.Instantiate`.
/// </summary>
public ReactiveValue<IImmutableList<IImmutableList<Renderer>>> TargetGroups { get; }
public ulong WhatChanged { get; }

/// <summary>
/// Performs any heavyweight operations required to prepare the renderers for preview. This method is called
/// once when the preview pipeline is set up. You can use the attached ComputeContext to arrange for the
/// preview pipeline to be recomputed when something changes with the initial state of the renderer.
/// Recreates this RenderFilterNode, with a new set of target renderers. The node _may_ reuse state, including
/// things such as output RenderTextures, from its prior run. It may also fast-fail and return null; in this
/// case, the preview pipeline will create a new node from its original `IRenderFilter` instead. Finally,
/// it may return itself; in this case, it will continue to be used with the new renderers.
///
/// This function is passed a list of original-proxy object pairs, which are guaranteed to have the same
/// original objects, in the same order, as the initial call to Instantiate, but will have new proxy objects.
/// It is also passed an update flags field, which indicates which upstream nodes have changed since the last
/// update. This may be zero if the update was triggered by an invalidation on the compute context for this
/// node itself.
///
/// As with `IRenderFilter.Instantiate`, the OnFrame effects of prior stages in the pipeline will be applied
/// before invoking this function. This ensures any changes to bones, blendshapes, etc will be reflected in this
/// mesh.
///
/// This function must not destroy the original Node. If it chooses to share resources with the original node,
/// those resources must not be released until both old and new nodes are destroyed.
/// </summary>
/// <param name="state"></param>
/// <param name="proxyPairs"></param>
/// <param name="context"></param>
/// <param name="updateFlags"></param>
/// <returns></returns>
public Task MutateMeshData(IList<MeshState> state, ComputeContext context)
public Task<IRenderFilterNode> Refresh(
IEnumerable<(Renderer, Renderer)> proxyPairs,
ComputeContext context,
ulong updateFlags
)
{
return Task.CompletedTask;
return Task.FromResult<IRenderFilterNode>(null);
}

/// <summary>
/// Called on each frame to perform lighter-weight operations on the renderers, such as manipulating blend shapes
/// or the bones array.
/// Invoked on each frame, and may modify the target renderers bound to this render filter node. Generally,
/// you should not modify the mesh or materials in this method, but you may change other properties, such as
/// the bones array, blend shapes, or the active state of the renderer. These properties will be reset to the
/// original renderer state on each frame.
///
/// This function is passed the original and replacement renderers, and is invoked for each renderer in question.
/// If an original renderer is destroyed, OnFrame will be called only on remaining renderers, until the preview
/// pipeline rebuild is completed.
/// </summary>
/// <param name="original"></param>
/// <param name="proxy"></param>
public void OnFrame(Renderer original, Renderer proxy)
{
}

void IDisposable.Dispose()
{
}
}
}
119 changes: 119 additions & 0 deletions Editor/PreviewSystem/Rendering/NodeController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#region

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using nadena.dev.ndmf.rq;
using UnityEngine;

#endregion

namespace nadena.dev.ndmf.preview
{
internal class NodeController : IDisposable
{
private class RefCount
{
public int Count = 1;
}

private readonly IRenderFilter _filter;
private readonly IRenderFilterNode _node;
private readonly List<(Renderer, ProxyObjectController)> _proxies;
private readonly RefCount _refCount;
internal ulong WhatChanged = IRenderFilterNode.Everything;

internal ProxyObjectController GetProxyFor(Renderer r)
{
return _proxies.Find(p => p.Item1 == r).Item2;
}

private NodeController(
IRenderFilterNode node,
List<(Renderer, ProxyObjectController)> proxies,
RefCount refCount
)
{
_node = node;
_proxies = proxies;
_refCount = refCount;

OnFrame();
}

internal void OnFrame()
{
foreach (var (original, proxy) in _proxies)
{
if (original != null && proxy.Renderer != null)
{
_node.OnFrame(original, proxy.Renderer);
}
}
}

public static async Task<NodeController> Create(
IRenderFilter filter,
List<(Renderer, ProxyObjectController)> proxies)
{
var invalidater = new TaskCompletionSource<object>();

ComputeContext context = new ComputeContext(() => filter.ToString());
context.Invalidate = () => invalidater.SetResult(null);
context.OnInvalidate = invalidater.Task;

var node = await filter.Instantiate(
proxies.Select(p => (p.Item1, p.Item2.Renderer)),
context
);

return new NodeController(node, proxies, new RefCount());
}

public async Task<NodeController> Refresh(
List<(Renderer, ProxyObjectController)> proxies,
ulong changes
)
{
var invalidater = new TaskCompletionSource<object>();

ComputeContext context = new ComputeContext(() => _node.ToString());
context.Invalidate = () => invalidater.SetResult(null);
context.OnInvalidate = invalidater.Task;

var node = await _node.Refresh(
proxies.Select(p => (p.Item1, p.Item2.Renderer)),
context,
changes
);

RefCount refCount;
if (node == _node)
{
refCount = _refCount;
refCount.Count++;
}
else if (node == null)
{
return await Create(_filter, proxies);
}
else
{
refCount = new RefCount();
}

var controller = new NodeController(node, proxies, refCount);
controller.WhatChanged = changes | node.WhatChanged;
return controller;
}

public void Dispose()
{
if (--_refCount.Count == 0)
{
_node.Dispose();
}
}
}
}
3 changes: 3 additions & 0 deletions Editor/PreviewSystem/Rendering/NodeController.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 83761b0

Please sign in to comment.