How to Implement Procedural Surfaces

This tutorial describes the process of implementing procedural surfaces with Microsoft Visual Studio.

  1. Create a new Active Template Library (ATL) project from Microsoft Visual C++ by opening the File menu and choosing the New command. Select the Projects tab on the dialog box, and then select ATL COM AppWizard from the list of projects.

  2. Enter the new project name and click OK.

  3. Set the server type by selecting the Dynamic Link Library option and click Finish.

  4. Open Stdafx.h and insert the following code immediately after the include statement for Atlcom.h.

    #include <ocidl.h>
    #include <dxtrans.h>
    #include <dxbounds.h>
    #include <dxhelper.h>
    #include <dxsurfb.h>
    #include <atlctl.h>
    
  5. Open Stdafx.cpp and insert the following include statements as the last lines of the file immediately after the include of Atlimpl.cpp.

    #include <dxtguid.c>
    #include <dtbase.cpp>
    #include <dxsurfb.cpp>
    #include <atlctl.cpp>
    
  6. Open the project .idl file and add the following line after the import of Ocidl.idl.

    import "Dxtrans.idl";
    
  7. On the Project menu, click Settings. Select the Link tab, and enter Dxtrans.lib in the Project Options list, just before /nologo. Click OK.

  8. Build the project. If the compiler cannot find one or more of the files, you have not yet added the Microsoft DirectX Transform file directories to your path. You can do one of the following:

    • Change your global settings to always use the Microsoft DirectX Transform directory. On the Tools menu, click Options. Click the Directories tab and enter the include and lib directories for the corresponding categories. Make sure each directory is the first entry in its list.
    • Set the Microsoft DirectX Transform directory for only this project. On the Project menu, choose Settings. In the Settings For drop-down list, select All Configurations. Click the C/C++ tab, click Category of Preprocessor, enter the directory in the Additional Include Directories control, and click OK.
  9. Add a new ATL object to the project after you confirm that the project builds and links.

    • On the Insert menu, choose New ATL Object.
    • In the dialog box that appears, select Simple Object and click Next.
    • From the Names tab, enter a name for the object in the Short Name text field—named "MySurface" in the following example.
    • Select the Attributes tab.
    • Under Threading Model, select Both.
    • Under Interface, select Dual.
    • Under Aggregation, select Yes.
    • Select the Free Threaded Marshaler check box.
    • Click OK.
  10. Open the MySurface.h file and make the following changes.

    • Make your class inherit from the necessary base classes by replacing this inheritance:

      public CComObjectRootEx<CComMultiThreadModel>,
      public CComCoClass<CMySurface, &CLSID_MySurface>,
      public IDispatchImpl<IMySurface, &IID_IMySurface, &LIBID_MYPROJECTLib>
      

      with the following lines.

      public CComCoClass<CMySurface, &CLSID_MySurface>,
      public IDispatchImpl<IMySurface, &IID_IMySurface, &LIBID_MYPROJECTLib>,
      public CComPropertySupport<CMySurface>,
      public IPersistStorageImpl<CMySurface>,
      public ISpecifyPropertyPagesImpl<CMySurface>,
      public IPersistPropertyBagImpl<CMySurface>,
      public CDXBaseSurface,
      public IDXTScaleOutput
      
    • Enter the following lines of code immediately before END_COM_MAP in the COM_MAP.

      COM_INTERFACE_ENTRY_IMPL(IPersistStorage)
      COM_INTERFACE_ENTRY_IMPL(ISpecifyPropertyPages)
      COM_INTERFACE_ENTRY_IMPL(IPersistPropertyBag)
      COM_INTERFACE_ENTRY(IDXTScaleOutput)
      COM_INTERFACE_ENTRY_CHAIN(CDXBaseSurface)
      
    • Enter the following lines of code for the property map after END_COM_MAP.

      BEGIN_PROPERTY_MAP(CMySurface)
      END_PROPERTY_MAP()        
      
    • Change the registration entry from the following:

      DECLARE_REGISTRY_RESOURCEID(IDR_MYSURFACE) 
      

      to:

      DECLARE_REGISTER_DX_SURFACE(IDR_MYSURFACE)
      DECLARE_POLY_AGGREGATABLE(CMySurface)
      DECLARE_PROTECT_FINAL_CONSTRUCT()
      
    • Enter the following prototype statements as part of your public method declarations.

      const GUID & SurfaceCLSID() { return GetObjectCLSID(); }
      HRESULT CreateARGBPointer(CDXBaseSurface * pSurface, CDXBaseARGBPtr ** ppPtr);
      void DeleteARGBPointer(CDXBaseARGBPtr *pPtr);
      STDMETHODIMP SetOutputSize(const SIZE OutputSize, BOOL bMaintainAspect);        
      
    • Enter a class declaration at the end of the header file for an object that derives from CDXBaseARGBPtr.

      class MySurfacePtr : public CDXBaseARGBPtr
      {
      public:
          MySurfacePtr(CDXBaseSurface * pSurface) : CDXBaseARGBPtr(pSurface) {}
          void FillSamples(const DXPtrFillInfo & FillInfo);
      public:
      };
      
  11. Use the following code to implement the CreateARGBPointer and DeleteARGBPointer methods. This implementation should go in the MySurface.cpp file.

    HRESULT CMySurface::CreateARGBPointer(CDXBaseSurface * pSurface,
                                          CDXBaseARGBPtr ** ppPtr)
    {
        *ppPtr = new MySurfacePtr(this);
        if (*ppPtr)
       {
            return S_OK;
        }
        else
        {
            return E_OUTOFMEMORY;
        }
    }
    
    void CMySurface::DeleteARGBPointer(CDXBaseARGBPtr *pPtr)
    {
        delete (MySurfacePtr *)pPtr;
    }
    
  12. Add the core of the program.

    You need to write three main section of code: the surface constructor, the CMySurface::SetOutputSize method, and the MySurfacePtr::FillSamples method.

    • The Surface Constructor

      In the body of the constructor for your object, you must initialize the data members to configure your surface. In addition to the data members that represent the custom properties of the surface, you also need to set the inherited m_Width and m_Height variables that define the size of your surface.

    • CMySurface::SetOutputSize

      This method is called by the base class to change the size of the surface. If you need to recalculate any internal data members because of a change to the surface size, do so before the method returns.

    • MySurfacePtr::FillSamples

      This method creates the surface one row of samples at a time. It can be called repeatedly by the base class to read the samples of the entire surface or just once to read a portion of an arbitrary row.

      The samples you need to calculate for the row are defined by a DXPtrFillInfo structure that is passed as a parameter to the method. It contains the x-axis and y-axis position of the first sample, the number of samples in the row to fill, and a pointer to the output sample array. There is also an option for the surface user to request the sample output in either ARGB32 or PMARGB32 format. You need to write routines that check for this and produce samples in the requested output format.

  13. If your surface requires custom properties to function, add a property page.

    • On the Insert menu, click New ATL Object.
    • On the dialog box that appears, select Controls in the left window, select Property Page in the right window, and click Next.
    • From the Names tab, enter a name for the object in the Short Name text field—named MySurfacePP in the following example.
    • Select the Attributes tab.
    • Under Threading Model, select Both.
    • Under Interface, select Dual.
    • Under Aggregation, select Yes.
    • Select the Free Threaded Marshaler check box.
    • Click OK.

    In the MySurface.h file, enter the following code in the PROPERTY_MAP statement, just before the END_PROPERTY_MAP statement.

    PROP_PAGE(CLSID_MySurfacePP)
    
  14. Add support for the IDispatch interface.

    For each private data member of your surface that represents a custom property, you need to implement put_ and get_ access functions. These are often declared in your MySurface.h header file in the following manner.

        STDMETHOD( get_MyProperty )( float *pVal );
        STDMETHOD( put_MyProperty )( float newVal );
    

    Add dispatch interface support for the MySurface interface. Create an enumeration for the dispatcher identifiers, with each property as an element of the enumeration. Place it just before the entry for MyEffect in the project's .idl file.

    typedef enum MyEffectDISPID
    {
        DISPID_MyEffect_MyProperty1 = DISPID_DXE_NEXT_ID,
        DISPID_MyEffect_MyProperty2,
    } MyEffectDISPID;
    

    Add dispatcher methods to the MyEffect interface, as shown in the following code.

    interface MyEffect : IDXEffect
    {
        [propget, id(DISPID_MyEffect_MyProperty1), helpstring("property MyProperty1")] 
        HRESULT MyProperty1([out, retval] float *pVal);
        [propput, id(DISPID_MyEffect_MyProperty1), helpstring("property MyProperty1")] 
        HRESULT MyProperty1([in] float newVal);
        [propget, id(DISPID_MyEffect_MyProperty2), helpstring("property MyProperty2")] 
        HRESULT MyProperty2([out, retval] float *pVal);
        [propput, id(DISPID_MyEffect_MyProperty2), helpstring("property MyProperty2")] 
        HRESULT MyProperty2([in] float newVal);
    };
    
  15. Implement the property page.

    • Inside the resource file for the project, there should be a dialog box entry for your property page. You need to edit this to reflect the custom properties that users can change on your surface.
    • In the MySurfacePP.cpp file, override the OnInitDialog method. It should use the get_MyProperty method for each property to set the initial values of the dialog box.
    • In the MySurfacePP.cpp file, override the Apply method. It should read each value from the dialog box and use the put_MyProperty method to set the values for the surface.