Creating Material Texture and Retaining Pixels

I have been quiet now for a while in shock and grieving about violence and racism in the world.

Meanwhile, a bunch of interesting discussions on creating material with texture, modifying element level, cutting off image pixels and other things:

Creating a Material with Texture in Revit and Forge

This topic has been very much en vogue lately. It came up again in the context of Forge in the StackOverflow question on creating a material with texture in Autodesk Revit Forge Design Automation, where maleficca very kindly shares a complete solution for both environments:

Question: I'm currently working on some Revit API code which is running in the Autodesk Forge Design Automation cloud solution.

Basically, I'm trying to create a material and attach a texture to it via the following code:

  private void AddTexturePath( 
    AssetProperty asset,
    string texturePath )
  {
    Asset connectedAsset = null;

    if( asset.NumberOfConnectedProperties == 0 )
      asset.AddConnectedAsset( "UnifiedBitmapSchema" );

    connectedAsset = (Asset) asset.GetConnectedProperty( 0 );

    AssetPropertyString path 
      = (AssetPropertyString) connectedAsset.FindByName(
        UnifiedBitmap.UnifiedbitmapBitmap );

    if( !path.IsValidValue( texturePath ) )
    {
      File.Create( "texture.png" );
      texturePath = Path.GetFullPath( "texture.png" );
    }
    path.Value = texturePath;
  }

This is actually working well, as the value for the texture path:

  path.Value = texturePath;

Needs to be a reference to an existing file. I do not have this file on the cloud instance of Forge, because the path to the texture name is specified by the user when he sends the request for the Workitem.

The problem is that this sets the texture path for the material as something like this:

  T:\Aces\Jobs\\texture.png

Which is basically the working folder for the Workitem instance. This path is useless, because a material with texture path like this needs to be manually re-linked in Revit.

The perfect outcome for me would be if I could somehow map the material texture path to some user-friendly directory like C:\Textures\texture.png and it seems that the Forge instance has a C:\ drive present (being probably a Windows instance of some sorts), but my code runs on low privileges, so it cannot create any kind of directories or files outside the working directory.

Does somebody have any idea how this could be resolved? Any help would be greatly appreciated!

Answer: Congratulations on getting to this point. Would you like to share the code you use to create the material and attach the texture for the Revit API add-in developer community to enjoy, either here or in a new thread in the Revit API discussion forum? People keep asking for such samples... Thank you!

Response: Here is my own answer and code sample:

After a whole day of research, I pretty much arrived at a satisfying solution. Just for clarity – I am going to reference to Autodesk Forge Design Automation API for Revit, simply as "Forge".

Basically, the code provided above is correct. I did not find any possible way to create a file on Forge instance, in a directory different than the Workitem working directory which is:

  T:\Aces\Jobs\\texture.png

Interestingly, there is a C:\ drive on the Forge instance, which contains Windows, Revit and .NET Framework installations (as Forge instance is basically some sort of Windows instance with Revit installed). It is possible to enumerate a lot of these directories, but none of the ones I've tried (and I've tried a lot – mostly the most obvious, public access Windows directories like C:\Users\Public, C:\Program Files, etc.) allow for creation of directories or files. This corresponds to what is stated in "Restrictions" area of the Forge documentation:

Your application is run with low privileges, and will not be able to freely interact with the Windows OS:

So, after trying to save the "dummy" texture file somewhere on the Forge C:\ drive, I've found another solution – the texture path for your texture actually does not matter.

This is because Revit offers an alternative for re-linking your textures. If you fire up Revit, you can go to File > Options > Rendering, and under "Additional render appearance paths" field, you can specify the directories on your local machine, that Revit can use to look for missing textures. With these, you can do the following operations in order to have full control on creating materials on Forge:

  1. Send Workitem to Forge, create the materials.
  2. Create a dummy texture in working directory, with the correct file name.
  3. Attach the dummy texture file to the material.
  4. Output the resulting file (.rvt or .rfa, depending on what you're creating on Forge).
  5. Place all textures into one folder (or multiple, this doesn't matter that much).
  6. Add the directories with the textures to the Additional render appearance paths.
  7. Revit will successfully re-link all the textures to new paths.

I hope someone will find this useful!

Additionally, as per Jeremy's request, I post a code sample for creating material with texture and modifying different Appearance properties in Revit by using Revit API (in C#):

  private void SetAppearanceParameters(
    Document project,
    Material mat, 
    MaterialData data )
  {
    usingTransaction setParameters = new Transaction( 
      project, "Set material parameters" ) )
    {
      setParameters.Start();

      AppearanceAssetElement genericAsset 
        = new FilteredElementCollector( project )
          .OfClass( typeofAppearanceAssetElement ) )
          .ToElements()
          .Cast<AppearanceAssetElement>().Where( i
            => i.Name.Contains( "Generic" ) )
          .FirstOrDefault();

      AppearanceAssetElement newAsset 
        = genericAsset.Duplicate( data.Name );

      mat.AppearanceAssetId = newAsset.Id;

      usingAppearanceAssetEditScope editAsset 
        = new AppearanceAssetEditScope( project ) )
      {
        Asset editableAsset = editAsset.Start( newAsset.Id );

        AssetProperty assetProperty 
          = editableAsset[ "generic_diffuse" ];

        SetColor( editableAsset, data.MaterialAppearance.Color );
        SetGlossiness( editableAsset, data.MaterialAppearance.Gloss );
        SetReflectivity( editableAsset, data.MaterialAppearance.Reflectivity );
        SetTransparency( editableAsset, data.MaterialAppearance.Transparency );

        if( data.MaterialAppearance.Texture != null
          && data.MaterialAppearance.Texture.Length != 0 )
        {
          AddTexturePath( assetProperty, 
            $@"C:\{data.MaterialIdentity.Manufacturer}\textures\{data.MaterialAppearance.Texture}" );
        }
        editAsset.Commit( true );
      }
      setParameters.Commit();
    }
  }

  private void SetTransparency(
    Asset editableAsset,
    int transparency )
  {
    AssetPropertyDouble genericTransparency 
      = editableAsset[ "generic_transparency" ]
        as AssetPropertyDouble;

    genericTransparency.Value = Convert.ToDouble( 
      transparency );
  }

  private void SetReflectivity( 
    Asset editableAsset,
    int reflectivity )
  {
    AssetPropertyDouble genericReflectivityZero
      = (AssetPropertyDouble) editableAsset[ 
        "generic_reflectivity_at_0deg" ];

    genericReflectivityZero.Value = Convert.ToDouble( 
      reflectivity ) / 100;

    AssetPropertyDouble genericReflectivityAngle
      = (AssetPropertyDouble) editableAsset[
        "generic_reflectivity_at_90deg" ];

    genericReflectivityAngle.Value = Convert.ToDouble( 
      reflectivity ) / 100;
  }

  private void SetGlossiness( 
    Asset editableAsset, 
    int gloss )
  {
    AssetPropertyDouble glossProperty 
      = (AssetPropertyDouble) editableAsset[ 
        "generic_glossiness" ];

    glossProperty.Value = Convert.ToDouble( 
      gloss ) / 100;
  }

  private void SetColor(
    Asset editableAsset,
    int[] color )
  {
    AssetPropertyDoubleArray4d genericDiffuseColor 
      = (AssetPropertyDoubleArray4d) editableAsset[ 
        "generic_diffuse" ];

    Color newColor = new Color( (byte) color[ 0 ], 
      (byte) color[ 1 ], (byte) color[ 2 ] );

    genericDiffuseColor.SetValueAsColor( newColor );
  }

  private void AddTexturePath( 
    AssetProperty asset, 
    string texturePath )
  {
    Asset connectedAsset = null;
    if( asset.NumberOfConnectedProperties == 0 )
      asset.AddConnectedAsset( "UnifiedBitmapSchema" );

    connectedAsset = (Asset) asset.GetConnectedProperty( 0 );

    AssetProperty prop = connectedAsset.FindByName( 
      UnifiedBitmap.UnifiedbitmapBitmap );

    AssetPropertyString path 
      = (AssetPropertyString) connectedAsset.FindByName( 
        UnifiedBitmap.UnifiedbitmapBitmap );

    string fileName = Path.GetFileName( texturePath );
    File.Create( fileName );
    texturePath = Path.GetFullPath( fileName );

    path.Value = texturePath;
  }

Hopefully it will come in handy to someone in the future!

Also, a huge thanks for The Building Coder; it has saved me a lot of hassle in my work with Revit API and enabled to create a lot of cool stuff!

A great thanks back to maleficca for kindly sharing the two solutions, both for Forge and the Revit desktop API!

Export Image Cutting off Pixels

This query just came up again in the Revit API discussion forum thread on export image cutting off edge pixels:

Question: I'm trying to export family type images from a family, but I'm getting the edge pixels cut off in my exported images.

Here is the original image:

Original image

This is what comes out:

Exported image

This is what I currently have for my export image options

  img = ImageExportOptions()
  img.ZoomType = ZoomFitType.FitToPage
  img.PixelSize = size
  img.ImageResolution = ImageResolution.DPI_150
  img.FitDirection = FitDirectionType.Vertical
  img.ExportRange = ExportRange.SetOfViews
  img.SetViewsAndSheets( viewIds )
  img.HLRandWFViewsFileType = ImageFileType.PNG
  img.FilePath = filepath
  img.ShadowViewsFileType = ImageFileType.PNG

A solution was provided by alexpaduroiu in a previous conversation on why export image is cutting a few pixels from image corners:

Question: I have a small problem regarding Decoument.ExportImage(ImageExportOptions options).

I am trying to export a set of drafting views, but somehow the generated images cut the views edges.

A sample image:

Incomplete image

This image has the bottom part not visible at all:

Incomplete image

The cut is very small but very frustrating because I can't make it a whole or even offset the element to fit the images.

Image Export Options used:

  ImageExportOptions imgExportOpts 
    = new ImageExportOptions()
    {
      ZoomType = ZoomFitType.FitToPage,
      PixelSize = 500,
      FilePath = rbrImagesDirectory + @"\",
      FitDirection = FitDirectionType.Vertical,
      HLRandWFViewsFileType = ImageFileType.PNG,
      ShadowViewsFileType = ImageFileType.PNG,
      ImageResolution = ImageResolution.DPI_72,
      ShouldCreateWebSite = false,
    };

  imgExportOpts.ExportRange = ExportRange.SetOfViews;

I have tried to modify FitDirectionType.Horizontal and this makes it worse than it is now by cutting the bottom portion even more; in that case, for the first image, the bottom part of the bar is not visible at all.

The image doesn't have such a big cut in the edges but it will be nice to have some spaces there or at least to see the parts   :-)

Is there any way to zoom out the element or move it in order to be arranged better in image?

Answer: Well, I have solved the problem!

The problem was with the drafting view I was trying to export. After creating a group of details in the drafting view, somehow the outline of the view wasn't big enough to include my details. don't know for sure why, but what I have done to resolve the problem was the following:

  drafting.CropBoxActive = true;
  drafting.CropBoxVisible = true;

private static void ExtendViewCrop(
  View drafting,
  Group detail )
{
  BoundingBoxXYZ crop = (drafting != null
    ? drafting.CropBox
    : null);

  if( crop == null || crop.Max == null || crop.Min == null
    || crop.Transform == null || detail == null )
  {
    return;
  }
  BoundingBoxXYZ detailBox = detail.get_BoundingBox( drafting );
  BoundingBoxXYZ extendedCrop = new BoundingBoxXYZ();
  extendedCrop.Transform = crop.Transform;

  if( detailBox == null || detailBox.Max == null
    || detailBox.Min == null )
  {
    return;
  }
  extendedCrop.Max = detailBox.Max
      + (extendedCrop.Transform.BasisX
      + extendedCrop.Transform.BasisY / 2) * 0.03;

  extendedCrop.Min = detailBox.Min
      + (-extendedCrop.Transform.BasisX
      + -extendedCrop.Transform.BasisY / 2) * 0.03;

  drafting.CropBox = extendedCrop;
}

So basically, extending the view crops max and min on their direction with a small value so the detail will fit in my drafting.

I am sure that there are lots of other better options doing this, but for now it did the trick.

Of course I am open to more solutions   :-)

Many thanks to alexpaduroiu for this clear solution.

Change Level of Existing Element

Angelo Mastroberardino brought up this question in his comment on creating a sloped floor:

Question: Is it possible to re-set the reference Level of a Floor, once it is created ?

Answer: In general, the Level property is read-only and thus cannot be set after an element has been created. It is specified during creation only and cannot be modified later.

Here are some posts on levels, both general level handling issues and workarounds to set the level property on certain specific element types:

Physics is Cool

A very nice and surprising physics experiment to try out at home:

Physics is cool

Forge Job Openings

Are you a critical thinker, problem solver, story teller, goal oriented, smart, curios, empathic, with experience in a cloud environment such as AWS?

If so, would you like to consider applying for a job in the Forge team?

Good luck applying for one of these or the many other opportunities that you can find all over the world in the Autodesk career site!